<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
<meta name="theme-color" content="#222">
<meta name="generator" content="Hexo 4.2.1">
  <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/images/%E6%AD%A6%E6%B1%8932x32.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/images/%E6%AD%A6%E6%B1%8916x16.png">
  <link rel="mask-icon" href="/images/logo.svg" color="#222">

<link rel="stylesheet" href="/css/main.css">


<link rel="stylesheet" href="/lib1/font-awesome/css/all.min.css">
  <link rel="stylesheet" href="/lib1/pace/pace-theme-center-simple.min.css">
  <script src="/lib1/pace/pace.min.js"></script>

<script id="hexo-configurations">
    var NexT = window.NexT || {};
    var CONFIG = {"hostname":"example.com","root":"/","scheme":"Gemini","version":"7.8.0","exturl":false,"sidebar":{"position":"left","display":"post","padding":18,"offset":12,"onmobile":false},"copycode":{"enable":true,"show_result":true,"style":null},"back2top":{"enable":true,"sidebar":false,"scrollpercent":false},"bookmark":{"enable":false,"color":"#222","save":"auto"},"fancybox":false,"mediumzoom":false,"lazyload":false,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"algolia":{"hits":{"per_page":10},"labels":{"input_placeholder":"Search for Posts","hits_empty":"We didn't find any results for the search: ${query}","hits_stats":"${hits} results found in ${time} ms"}},"localsearch":{"enable":true,"trigger":"auto","top_n_per_article":1,"unescape":false,"preload":false},"motion":{"enable":true,"async":false,"transition":{"post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}},"path":"search.xml"};
  </script>

  <meta property="og:type" content="website">
<meta property="og:title" content="Technological Blog">
<meta property="og:url" content="http://example.com/page/2/index.html">
<meta property="og:site_name" content="Technological Blog">
<meta property="og:locale" content="zh_CN">
<meta property="article:author" content="Li Yudong">
<meta name="twitter:card" content="summary">

<link rel="canonical" href="http://example.com/page/2/">


<script id="page-configurations">
  // https://hexo.io/docs/variables.html
  CONFIG.page = {
    sidebar: "",
    isHome : true,
    isPost : false,
    lang   : 'zh-CN'
  };
</script>

  <title>Technological Blog</title>
  






  <noscript>
  <style>
  .use-motion .brand,
  .use-motion .menu-item,
  .sidebar-inner,
  .use-motion .post-block,
  .use-motion .pagination,
  .use-motion .comments,
  .use-motion .post-header,
  .use-motion .post-body,
  .use-motion .collection-header { opacity: initial; }

  .use-motion .site-title,
  .use-motion .site-subtitle {
    opacity: initial;
    top: initial;
  }

  .use-motion .logo-line-before i { left: initial; }
  .use-motion .logo-line-after i { right: initial; }
  </style>
</noscript>

</head>

<body itemscope itemtype="http://schema.org/WebPage">
  <div class="container use-motion">
    <div class="headband"></div>

    <header class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner"><div class="site-brand-container">
  <div class="site-nav-toggle">
    <div class="toggle" aria-label="切换导航栏">
      <span class="toggle-line toggle-line-first"></span>
      <span class="toggle-line toggle-line-middle"></span>
      <span class="toggle-line toggle-line-last"></span>
    </div>
  </div>

  <div class="site-meta">

    <a href="/" class="brand" rel="start">
      <span class="logo-line-before"><i></i></span>
      <h1 class="site-title">Technological Blog</h1>
      <span class="logo-line-after"><i></i></span>
    </a>
      <p class="site-subtitle" itemprop="description">IT小白的成长之旅</p>
  </div>

  <div class="site-nav-right">
    <div class="toggle popup-trigger">
        <i class="fa fa-search fa-fw fa-lg"></i>
    </div>
  </div>
</div>




<nav class="site-nav">
  <ul id="menu" class="main-menu menu">
        <li class="menu-item menu-item-about">

    <a href="/about/" rel="section"><i class="user fa-fw"></i>关于</a>

  </li>
        <li class="menu-item menu-item-tags">

    <a href="/tags/" rel="section"><i class="tags fa-fw"></i>标签<span class="badge">18</span></a>

  </li>
        <li class="menu-item menu-item-categories">

    <a href="/categories/" rel="section"><i class="th fa-fw"></i>分类<span class="badge">14</span></a>

  </li>
        <li class="menu-item menu-item-archives">

    <a href="/archives/" rel="section"><i class="archive fa-fw"></i>归档<span class="badge">95</span></a>

  </li>
      <li class="menu-item menu-item-search">
        <a role="button" class="popup-trigger"><i class="fa fa-search fa-fw"></i>搜索
        </a>
      </li>
  </ul>
</nav>



  <div class="search-pop-overlay">
    <div class="popup search-popup">
        <div class="search-header">
  <span class="search-icon">
    <i class="fa fa-search"></i>
  </span>
  <div class="search-input-container">
    <input autocomplete="off" autocapitalize="off"
           placeholder="搜索..." spellcheck="false"
           type="search" class="search-input">
  </div>
  <span class="popup-btn-close">
    <i class="fa fa-times-circle"></i>
  </span>
</div>
<div id="search-result">
  <div id="no-result">
    <i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
  </div>
</div>

    </div>
  </div>

</div>
    </header>

    
  <div class="back-to-top">
    <i class="fa fa-arrow-up"></i>
    <span>0%</span>
  </div>

  <a href="https://github.com/tiarmor1" class="github-corner" title="Follow me on GitHub" aria-label="Follow me on GitHub" rel="noopener" target="_blank"><svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>


    <main class="main">
      <div class="main-inner">
        <div class="content-wrap">
          

          <div class="content index posts-expand">
            
      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/22/%E7%9B%92%E9%A9%AC%E6%8A%80%E6%9C%AF%E6%A0%88%E5%88%9D%E5%AD%A6/RaxWEB%E5%BA%94%E7%94%A8/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/22/%E7%9B%92%E9%A9%AC%E6%8A%80%E6%9C%AF%E6%A0%88%E5%88%9D%E5%AD%A6/RaxWEB%E5%BA%94%E7%94%A8/" class="post-title-link" itemprop="url">RaxWEB应用</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
    
              <time title="创建时间：2021-11-22 12:18:39" itemprop="dateCreated datePublished" datetime="2021-11-22T12:18:39+08:00">2021-11-22</time>
            </span>
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="far fa-calendar-check"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2021-11-24 16:00:06" itemprop="dateModified" datetime="2021-11-24T16:00:06+08:00">2021-11-24</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/%E7%9B%92%E9%A9%AC%E6%8A%80%E6%9C%AF%E6%A0%88/" itemprop="url" rel="index"><span itemprop="name">盒马技术栈</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="WEB应用"><a href="#WEB应用" class="headerlink" title="WEB应用"></a>WEB应用</h1><h2 id="多页应用"><a href="#多页应用" class="headerlink" title="多页应用"></a>多页应用</h2><h3 id="应用配置"><a href="#应用配置" class="headerlink" title="应用配置"></a>应用配置</h3><p>多页应用（MultiPage Application，MPA），指有多个独立的页面的应用，页面之间的跳转需要整页资源刷新。</p>
<h4 id="开启-MPA"><a href="#开启-MPA" class="headerlink" title="开启 MPA"></a>开启 MPA</h4><p>build.json 中通过 <code>mpa</code> 选项开启，目前 web/weex/kraken 支持 MPA 模式，开启 mpa 之后 <code>src/app.tsx</code> 将失效。</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"web"</span>: &#123;</span><br><span class="line">    <span class="attr">"mpa"</span>: <span class="literal">true</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"targets"</span>: [<span class="string">"web"</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="配置页面"><a href="#配置页面" class="headerlink" title="配置页面"></a>配置页面</h4><p>在 <code>app.json</code> 通过 <code>routes</code> 字段配置不同页面：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span>,</span><br><span class="line">      <span class="attr">"window"</span>: &#123;</span><br><span class="line">        <span class="attr">"title"</span>: <span class="string">"Home 页面"</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/About/index"</span></span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>页面 URL</p>
<p>默认情况下通过页面路径生成产物路径，比如 <code>pages/Home/index</code> 会被构建输出为 <code>build/web/home.html</code>，托管到服务端的地址即为 <code>web.html</code>。如果开发者有特殊需求（比如旧工程版本升级），可以通过 <code>name</code> 字段进行自定义：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"name"</span>: <span class="string">"myhome"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"name"</span>: <span class="string">"pages/about"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/About/index"</span></span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>如上配置后：</p>
<ul>
<li>Home 页面对应的产物路径为 <code>build/web/myhome.html</code>，托管后的地址即为 <code>/myhome.html</code></li>
<li>Aboue 页面对应的产物路径为 <code>build/web/pages/about.html</code>，托管后的地址即为 <code>/pages/about.html</code></li>
</ul>
<h4 id="window"><a href="#window" class="headerlink" title="window"></a>window</h4><p>可以设置应用的窗口表现，同时也支持针对每个页面设置窗口表现。目前已经支持的参数的有：</p>
<table>
<thead>
<tr>
<th align="left">属性</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody><tr>
<td align="left">title</td>
<td align="left">页面标题</td>
</tr>
</tbody></table>
<h4 id="tabBar"><a href="#tabBar" class="headerlink" title="tabBar"></a>tabBar</h4><p>配置式 TabBar</p>
<p>框架支持通过简单的配置渲染一个 TabBar 组件：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"name"</span>: <span class="string">"home"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"name"</span>: <span class="string">"about"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/About/index"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"name"</span>: <span class="string">"profile"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Profile/index"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/detail/index"</span></span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">"tabBar"</span>: &#123;</span><br><span class="line">    <span class="attr">"textColor"</span>: <span class="string">"#999"</span>,</span><br><span class="line">    <span class="attr">"selectedColor"</span>: <span class="string">"#666"</span>,</span><br><span class="line">    <span class="attr">"backgroundColor"</span>: <span class="string">"#f8f8f8"</span>,</span><br><span class="line">    <span class="attr">"items"</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">"pageName"</span>: <span class="string">"home"</span>,</span><br><span class="line">        <span class="attr">"icon"</span>: <span class="string">"https://gw.alicdn.com/tfs/TB1ypSMTcfpK1RjSZFOXXa6nFXa-144-144.png"</span>,</span><br><span class="line">        <span class="attr">"activeIcon"</span>: <span class="string">"https://gw.alicdn.com/tfs/TB1NBiCTgHqK1RjSZFPXXcwapXa-144-144.png"</span></span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">"pageName"</span>: <span class="string">"about"</span>,</span><br><span class="line">        <span class="attr">"icon"</span>: <span class="string">"https://gw.alicdn.com/tfs/TB1ypSMTcfpK1RjSZFOXXa6nFXa-144-144.png"</span>,</span><br><span class="line">        <span class="attr">"activeIcon"</span>: <span class="string">"https://gw.alicdn.com/tfs/TB1NBiCTgHqK1RjSZFPXXcwapXa-144-144.png"</span></span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">"path"</span>: <span class="string">"https://taobao.com"</span>,</span><br><span class="line">        <span class="attr">"pageName"</span>: <span class="string">"profile"</span>,</span><br><span class="line">        <span class="attr">"icon"</span>: <span class="string">"https://gw.alicdn.com/tfs/TB1ypSMTcfpK1RjSZFOXXa6nFXa-144-144.png"</span>,</span><br><span class="line">        <span class="attr">"activeIcon"</span>: <span class="string">"https://gw.alicdn.com/tfs/TB1NBiCTgHqK1RjSZFPXXcwapXa-144-144.png"</span></span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>tabBar 配置项如下：</p>
<table>
<thead>
<tr>
<th align="left">属性</th>
<th align="left">类型</th>
<th align="left">是否必填</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody><tr>
<td align="left">textColor</td>
<td align="left">HexColor</td>
<td align="left">否</td>
<td align="left">文字颜色</td>
</tr>
<tr>
<td align="left">selectedColor</td>
<td align="left">HexColor</td>
<td align="left">否</td>
<td align="left">选中文字颜色</td>
</tr>
<tr>
<td align="left">backgroundColor</td>
<td align="left">HexColor</td>
<td align="left">否</td>
<td align="left">背景色</td>
</tr>
<tr>
<td align="left">items</td>
<td align="left">Array</td>
<td align="left">是</td>
<td align="left">每个 tab 配置</td>
</tr>
</tbody></table>
<p>tab item 配置项如下：</p>
<table>
<thead>
<tr>
<th align="left">属性</th>
<th align="left">类型</th>
<th align="left">必填</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody><tr>
<td align="left">text</td>
<td align="left">String</td>
<td align="left">是</td>
<td align="left">tab item 上显示的文本</td>
</tr>
<tr>
<td align="left">pageName</td>
<td align="left">String</td>
<td align="left">否</td>
<td align="left">显示该 tab 的页面名称</td>
</tr>
<tr>
<td align="left">icon</td>
<td align="left">String</td>
<td align="left">否</td>
<td align="left">非选中状态图标路径</td>
</tr>
<tr>
<td align="left">activeIcon</td>
<td align="left">String</td>
<td align="left">否</td>
<td align="left">选中状态图标路径</td>
</tr>
<tr>
<td align="left">path</td>
<td align="left">String</td>
<td align="left">否</td>
<td align="left">跳转的三方链接（当配置了 path 时，pageName 不再生效）</td>
</tr>
</tbody></table>
<p>自定义 TabBar如下：</p>
<p>框架除了支持配置式 TabBar，还支持自定义 TabBar 组件：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"name"</span>: <span class="string">"home"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"name"</span>: <span class="string">"about"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/About/index"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/detail/index"</span></span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">"tabBar"</span>: &#123;</span><br><span class="line">    <span class="attr">"custom"</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">"list"</span>: [</span><br><span class="line">      <span class="string">"home"</span>,</span><br><span class="line">      <span class="string">"about"</span></span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在开启 <code>custom: true</code> 时，框架约束 <code>src/components/CustomTabBar/index.jsx</code> 导出的组件即为 TabBar 组件，<code>list</code> 字段表示在哪些页面需要显示 TabBar。</p>
<h4 id="调试阶段编译指定页面"><a href="#调试阶段编译指定页面" class="headerlink" title="调试阶段编译指定页面"></a>调试阶段编译指定页面</h4><p>随着多页应用页面变多，编译速度会受到影响，因此 rax-app 支持在开发阶段通过 <code>npm start -- --mpa-entry=home</code> 指定你需要编译的页面，如果存在多个页面可以通过逗号分隔：<code>npm start -- --mpa-entry=home,about</code>。</p>
<h3 id="页面入口"><a href="#页面入口" class="headerlink" title="页面入口"></a>页面入口</h3><p>MPA 场景下支持三种类型的页面入口：</p>
<ol>
<li>推荐导出组件</li>
<li>不推荐 自定义渲染</li>
<li>runApp</li>
</ol>
<h4 id="导出组件"><a href="#导出组件" class="headerlink" title="导出组件"></a>导出组件</h4><p>默认情况下，推荐在页面入口 <code>pages/Home/index.tsx</code> 中直接导出一个组件：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// pages/Home/index</span></span><br><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">Home</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>Home Page<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Home.getInitialProps = <span class="keyword">async</span> () =&gt; &#123;&#125;</span><br></pre></td></tr></table></figure>

<p>框架会调用 <code>render()</code> 方法渲染该页面组件，同时处理 SSR 相关逻辑。</p>
<h4 id="自定义渲染"><a href="#自定义渲染" class="headerlink" title="自定义渲染"></a>自定义渲染</h4><p>如果开发者希望自己调用 <code>render</code> 方法渲染某个页面，只需要<strong>不使用</strong> <code>export default</code> 导出该页面组件即可：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// pages/Home/index</span></span><br><span class="line"><span class="keyword">import</span> &#123; render, createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> DriverUniversal <span class="keyword">from</span> <span class="string">'driver-universal'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Home</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>Home Page<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">render(<span class="xml"><span class="tag">&lt;<span class="name">Home</span> /&gt;</span></span>, <span class="built_in">document</span>.body, &#123; <span class="attr">driver</span>: DriverUniversal &#125;);</span><br></pre></td></tr></table></figure>

<p>缺陷：</p>
<ul>
<li>不支持 SSR 场景下通过 <code>Home.getInitialProps</code> 获取初始数据</li>
<li>不支持定义 store 实例后自动包裹 <code>store.Provider</code></li>
</ul>
<h4 id="runApp"><a href="#runApp" class="headerlink" title="runApp"></a>runApp</h4><p>MPA 情况下如果页面功能复杂，希望复用 SPA 模式下 runApp 以及 app.json 相关能力，可以在对应目录下添加相关文件：</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">  ├── src/                   </span><br><span class="line">  │   └── pages/            </span><br><span class="line">  │       ├── Home/     		</span><br><span class="line">+ │       │   ├── app.js     # 自定义页面配置入口</span><br><span class="line">+ │       │   ├── app.json   # 页面级别的 app.json 配置</span><br><span class="line">- │       │   └── index.jsx</span><br></pre></td></tr></table></figure>

<p>与标准的 SPA 相比，<code>runApp()</code> 需要手动引入 <code>app.json</code> 配置：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="keyword">import</span> staticConfig <span class="keyword">from</span> <span class="string">'./app.json'</span>;</span><br><span class="line"></span><br><span class="line">runApp(</span><br><span class="line">  &#123;</span><br><span class="line">    app: &#123;</span><br><span class="line">      onShow() &#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'app show...'</span>);</span><br><span class="line">      &#125;,</span><br><span class="line">      onHide() &#123;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'app hide...'</span>);</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">+  staticConfig</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<p>接着在 app.json 中配置该页面的路由（即便只有一个也必须配置）：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"./index"</span></span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>src/app.json</code> 的页面入口需要修改为 <code>app.js</code>：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">     <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span></span><br><span class="line">     <span class="string">"source"</span>: <span class="string">"pages/Home/app"</span></span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>如果要使用 store 能力，需要通过 <code>app.addProvider</code> 自行包裹 <code>store.Provider</code>：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/Dashboard/app.tsx</span></span><br><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="keyword">import</span> staticConfig <span class="keyword">from</span> <span class="string">'./app.json'</span>;</span><br><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">'./store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> Provider = store.Provider;</span><br><span class="line"></span><br><span class="line">runApp(&#123;</span><br><span class="line">  app: &#123;</span><br><span class="line">+   addProvider: <span class="function">(<span class="params">&#123; children &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">+     <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">Provider</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">Provider</span>&gt;</span></span>;</span><br><span class="line">+   &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;, staticConfig);</span><br></pre></td></tr></table></figure>

<p>使用 <code>runApp()</code> 入口的缺陷：</p>
<ul>
<li>即便只有一个页面也会引入单页应用相关的依赖比如 <code>history</code></li>
</ul>
<p><strong>注意：</strong> 在使用 app.js 进行渲染的时候，即使该文件导出了一个组件，框架也不会再调用 <code>render</code> 方法渲染该组件</p>
<h3 id="定制-HTML-结构"><a href="#定制-HTML-结构" class="headerlink" title="定制 HTML 结构"></a>定制 HTML 结构</h3><p>框架提供了两种方式定制 HTML 的内容：</p>
<ol>
<li>推荐 在 <code>src/app.json</code> 中修改部分配置</li>
<li>不推荐 手动创建 <code>src/document/index.jsx</code> 文件</li>
</ol>
<h4 id="默认-HTML-内容"><a href="#默认-HTML-内容" class="headerlink" title="默认 HTML 内容"></a>默认 HTML 内容</h4><p>工程中内置的 <code>HTML</code> 模板结构为:</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">"utf-8"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">"viewport"</span> <span class="attr">content</span>=<span class="string">"width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span><span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">"root"</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h4 id="通过-app-json-定制"><a href="#通过-app-json-定制" class="headerlink" title="通过 app.json 定制"></a>通过 app.json 定制</h4><p>支持的配置项如下：</p>
<ul>
<li><code>metas</code>: 插入到 <code>&lt;head&gt;</code> 标签内</li>
<li><code>links</code>: 插入到业务样式之前</li>
<li><code>scripts</code>: 插入到业务脚本之前</li>
<li><code>window</code>: 配置页面标题</li>
</ul>
<p>具体使用方式如下：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span>,</span><br><span class="line">     <span class="attr">"window"</span>: &#123;</span><br><span class="line">        <span class="attr">"title"</span>: <span class="string">"Home Page"</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/about"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/About/index"</span>,</span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="comment">// 插入 meta 标签</span></span><br><span class="line">  <span class="attr">"metas"</span>: [</span><br><span class="line">    <span class="string">"&lt;meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\" /&gt;"</span></span><br><span class="line">  ],</span><br><span class="line">  <span class="comment">// 插入 link 标签</span></span><br><span class="line">  <span class="attr">"links"</span>: [</span><br><span class="line">    <span class="string">"&lt;link rel=\"dns-prefetch\" href=\"//g.alicdn.com\" /&gt;"</span></span><br><span class="line">  ],</span><br><span class="line">  <span class="comment">// 插入 script 标签</span></span><br><span class="line">  <span class="attr">"scripts"</span>: [</span><br><span class="line">    <span class="string">"&lt;script defer src=\"xxx/index.js\"&gt;&lt;/script&gt;"</span></span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">"window"</span>: &#123;</span><br><span class="line">    <span class="comment">// 设置整个应用的 title，优先级低于每个页面单独设置的 title</span></span><br><span class="line">    <span class="attr">"title"</span>: <span class="string">"Rax App"</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>配置式的方案目前仅支持统一定制各个页面的 HTML，对于不同页面的 HTML 有差异的场景我们正在支持中，也欢迎向我们反馈你的诉求。</p>
<h4 id="通过-Document-定制"><a href="#通过-Document-定制" class="headerlink" title="通过 Document 定制"></a>通过 Document 定制</h4><p>在 <code>src/document</code> 目录创建 <code>index.tsx</code> 文件, 可以通过 jsx 的方式定制 HTML 结构：</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; Root, Style, Script &#125; <span class="keyword">from</span> <span class="string">'rax-document'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Document</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;html&gt;</span><br><span class="line">      &lt;head&gt;</span><br><span class="line">        &lt;meta charset=<span class="string">"utf-8"</span> /&gt;</span><br><span class="line">        &lt;meta name=<span class="string">"viewport"</span> content=<span class="string">"width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover"</span>/&gt;</span><br><span class="line">        &#123;<span class="comment">/* props.title 的值是通过 app.json 分析注入的 */</span>&#125;</span><br><span class="line">        &lt;title&gt;&#123;props.title&#125;&lt;<span class="regexp">/title&gt;</span></span><br><span class="line"><span class="regexp">        &lt;Style /</span>&gt;</span><br><span class="line">      &lt;<span class="regexp">/head&gt;</span></span><br><span class="line"><span class="regexp">      &lt;body&gt;</span></span><br><span class="line"><span class="regexp">        &#123;/</span>* root container *<span class="regexp">/&#125;</span></span><br><span class="line"><span class="regexp">        &lt;Root /</span>&gt;</span><br><span class="line">        &lt;Script /&gt;</span><br><span class="line">      &lt;<span class="regexp">/body&gt;</span></span><br><span class="line"><span class="regexp">    &lt;/</span>html&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> Document;</span><br></pre></td></tr></table></figure>

<p>组件说明：</p>
<ul>
<li><code>Root</code> 组件对应页面的根节点，<code>id</code> 为 <code>root</code></li>
<li><code>Style</code> 组件对应页面的样式资源，由工程插件自动分析页面依赖产出</li>
<li><code>Script</code> 组件对应页面的 JS 资源，由工程插件自动分析页面依赖产出</li>
</ul>
<p>如果希望为不同页面渲染差异化的 HTML，可以通过 <code>App</code> 组件来实现。<code>App</code> 接收的 <code>children</code> 为一个 <code>function</code>，其入参为项目 <code>app.json</code> 下该路由对应的页面信息。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; Root, Style, Script, App &#125; <span class="keyword">from</span> <span class="string">'rax-document'</span>;</span><br><span class="line"><span class="keyword">import</span> appJSON <span class="keyword">from</span> <span class="string">'../app.json'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Document</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;App config=&#123;appJSON&#125;&gt;</span><br><span class="line">      &#123;</span><br><span class="line">        pageInfo =&gt; &#123;</span><br><span class="line">          <span class="built_in">console</span>.log(<span class="string">'Here is pageInfo'</span>, pageInfo);</span><br><span class="line">          <span class="keyword">return</span> (</span><br><span class="line">            &lt;html&gt;</span><br><span class="line">              &lt;head&gt;</span><br><span class="line">                &lt;Style /&gt;</span><br><span class="line">              &lt;<span class="regexp">/head&gt;</span></span><br><span class="line"><span class="regexp">              &lt;body&gt;</span></span><br><span class="line"><span class="regexp">                &lt;Root /</span>&gt;</span><br><span class="line">              &lt;<span class="regexp">/body&gt;</span></span><br><span class="line"><span class="regexp">            &lt;/</span>html&gt;</span><br><span class="line">          );</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &lt;<span class="regexp">/App&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">export default Document;</span></span><br></pre></td></tr></table></figure>

<p>Document 语法限制：</p>
<p>由于 Document 使用的是 JSX 语法，而非普通的 HTML。所以，在 Document 中添加 style 或 script 需要结合 <code>dangerouslySetInnerHTML</code> 的方式，示例如下：</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&lt;style dangerouslySetInnerHTML=&#123;&#123;<span class="attr">__html</span>: <span class="string">`</span></span><br><span class="line"><span class="string">  p &#123;</span></span><br><span class="line"><span class="string">    color: red;</span></span><br><span class="line"><span class="string">    font-size: 20px;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">`</span>&#125;&#125;&gt;</span><br><span class="line">&lt;<span class="regexp">/style&gt;</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">&lt;script type="text/</span>javascript<span class="string">" dangerouslySetInnerHTML=&#123;&#123;__html: `</span></span><br><span class="line"><span class="string">  console.log("</span>Hello World!<span class="string">")</span></span><br><span class="line"><span class="string">`&#125;&#125;&gt;</span></span><br><span class="line"><span class="string">&lt;/script&gt;</span></span><br></pre></td></tr></table></figure>

<h3 id="状态管理"><a href="#状态管理" class="headerlink" title="状态管理"></a>状态管理</h3><p>rax-app 内置了状态管理方案，并在此基础上进一步遵循 <strong>“约定优于配置”</strong> 原则，进行抽象和封装，使得状态管理变得非常容易。<strong>约定大于配置。</strong></p>
<p><strong>注意：</strong> 如果开发者没有依照约定的目录结构创建 <code>store</code>，框架将不会注入任何状态管理相关的能力。</p>
<h4 id="使用指南"><a href="#使用指南" class="headerlink" title="使用指南"></a>使用指南</h4><h5 id="目录结构"><a href="#目录结构" class="headerlink" title="目录结构"></a>目录结构</h5><p>每个页面的状态位于 <code>src/pages/Dashboard/models/</code> 目录，具体的目录结构如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">src&#x2F;                       # 源码</span><br><span class="line">└── pages&#x2F;                 # 页面</span><br><span class="line">    ├── Dashboard&#x2F;         # Dashboard 页面</span><br><span class="line">    │   ├── models         # Dashboard 页面状态</span><br><span class="line">    │   |   ├── counter.ts</span><br><span class="line">    │   ├── store.ts       # Dashboard 页面级别的自定义 store</span><br><span class="line">    │   └── index.tsx</span><br><span class="line">    └── Home&#x2F;</span><br></pre></td></tr></table></figure>

<h5 id="定义模型状态"><a href="#定义模型状态" class="headerlink" title="定义模型状态"></a>定义模型状态</h5><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/pages/Dashboard/models/counter.ts</span></span><br><span class="line"><span class="keyword">const</span> delay = <span class="function">(<span class="params">time</span>) =&gt;</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve</span>) =&gt;</span> setTimeout(<span class="function"><span class="params">()</span> =&gt;</span> resolve(), time));</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="comment">// 定义 model 的初始 state</span></span><br><span class="line">  state: &#123;</span><br><span class="line">    count: <span class="number">0</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// 定义改变该模型状态的纯函数</span></span><br><span class="line">  reducers: &#123;</span><br><span class="line">    increment(prevState) &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123; <span class="attr">count</span>: prevState.count + <span class="number">1</span> &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">    decrement(prevState) &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123; <span class="attr">count</span>: prevState.count - <span class="number">1</span> &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">  effects: <span class="function">(<span class="params">dispatch</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="keyword">async</span> decrementAsync() &#123;</span><br><span class="line">      <span class="keyword">await</span> delay(<span class="number">10</span>);</span><br><span class="line">      dispatch.counter.decrement();</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;),</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h5 id="创建-Store-实例"><a href="#创建-Store-实例" class="headerlink" title="创建 Store 实例"></a>创建 Store 实例</h5><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/pages/Dashboard/store.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; createStore &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="keyword">import</span> counter <span class="keyword">from</span> <span class="string">'./models/counter'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> store = createStore(&#123; counter &#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> store;</span><br></pre></td></tr></table></figure>

<h5 id="View-中使用模型状态"><a href="#View-中使用模型状态" class="headerlink" title="View 中使用模型状态"></a>View 中使用模型状态</h5><p>定义 store 实例之后，框架会将对应的 <code>store.Provider</code> 包裹在页面组件上，因此我们可以直接在页面组件以及子组件里使用状态：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/pages/Dashboard/index.tsx</span></span><br><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">'./store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">Dashboard</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// 使用 counter 模型</span></span><br><span class="line">  <span class="keyword">const</span> [counterState, counterDispatchers] = store.useModel(<span class="string">'counter'</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;&gt;</span><br><span class="line">      &lt;span&gt;&#123;counterState.count&#125;&lt;<span class="regexp">/span&gt;</span></span><br><span class="line"><span class="regexp">      &lt;button onClick=&#123;counterDispatchers.increment&#125;&gt;+&lt;/</span>button&gt;</span><br><span class="line">      &lt;button onClick=&#123;counterDispatchers.decrementAsync&#125;&gt;-&lt;<span class="regexp">/button&gt;</span></span><br><span class="line"><span class="regexp">    &lt;/</span>&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="模型规范"><a href="#模型规范" class="headerlink" title="模型规范"></a>模型规范</h4><h5 id="模型定义"><a href="#模型定义" class="headerlink" title="模型定义"></a>模型定义</h5><p>通常模型定义包括 state、reducers、effects 三部分：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  state: &#123;&#125;,</span><br><span class="line"></span><br><span class="line">  reducers: &#123;&#125;,</span><br><span class="line"></span><br><span class="line">  effects: &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h5 id="state"><a href="#state" class="headerlink" title="state"></a>state</h5><p>model 的初始 state</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  state: &#123; <span class="attr">count</span>: <span class="number">0</span> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h5 id="reducers"><a href="#reducers" class="headerlink" title="reducers"></a>reducers</h5><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">reducers: &#123; [string]: <span class="function">(<span class="params">prevState, payload</span>) =&gt;</span> any &#125;</span><br></pre></td></tr></table></figure>

<p>一个改变该模型状态的函数集合。这些方法以模型的上一次 prevState 和一个 payload 作为入参，在方法中使用可变的方式来更新状态。这些方法应该是仅依赖于 prevState 和 payload 参数来计算下一个 nextState 的纯函数。对于有副作用的函数，请使用 effects。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  state: &#123; <span class="attr">count</span>: <span class="number">0</span> &#125;,</span><br><span class="line"></span><br><span class="line">+ reducers: &#123;</span><br><span class="line">+   increment (prevState, payload) &#123;</span><br><span class="line">+     <span class="keyword">return</span> &#123; <span class="attr">count</span>: prevState.count + <span class="number">1</span> &#125;;</span><br><span class="line">+   &#125;,</span><br><span class="line">+   decrement (prevState) &#123;</span><br><span class="line">+     <span class="keyword">return</span> &#123; <span class="attr">count</span>: prevState.count - <span class="number">1</span> &#125;;</span><br><span class="line">+   &#125;</span><br><span class="line">+ &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h5 id="effects"><a href="#effects" class="headerlink" title="effects"></a>effects</h5><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">effects: <span class="function">(<span class="params">dispatch</span>) =&gt;</span> <span class="function">(<span class="params">&#123; [string]: (payload, rootState</span>) =&gt;</span> <span class="keyword">void</span> &#125;)</span><br></pre></td></tr></table></figure>

<p>一个可以处理该模型副作用的函数集合。这些方法以 payload 和 rootState 作为入参，适用于进行异步调用、模型联动等场景。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  state: &#123; <span class="attr">count</span>: <span class="number">0</span> &#125;,</span><br><span class="line"></span><br><span class="line">  reducers: &#123;</span><br><span class="line">    increment (prevState, payload) &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123; <span class="attr">count</span>: prevState.count + <span class="number">1</span> &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">    decrement (prevState) &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123; <span class="attr">count</span>: prevState.count - <span class="number">1</span> &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">+ effects: <span class="function">(<span class="params">dispatch</span>) =&gt;</span> (&#123;</span><br><span class="line">+   <span class="keyword">async</span> asyncDecrement() &#123;</span><br><span class="line">+     <span class="keyword">await</span> delay(<span class="number">1000</span>);             <span class="comment">// 进行一些异步操作</span></span><br><span class="line">+     dispatch.counter.increment();  <span class="comment">// 调用模型 reducers 内的方法来更新状态</span></span><br><span class="line">+   &#125;,</span><br><span class="line">+ &#125;),</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>在 effects 中的 action 方法中可以通过 <code>dispatch[model][action]</code> 拿到其他模型所定义的方法。</p>
<h5 id="模型使用"><a href="#模型使用" class="headerlink" title="模型使用"></a>模型使用</h5><p>定义好 model 之后，我们就可以通过 <code>useModel</code> 传入对应的 model 名称进行使用，并返回该模型的状态（state）和调度器（dispatchers）。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">useModel(name: string): [ state, dispatchers ]</span><br><span class="line">+ <span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">'./store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> () =&gt; &#123;</span><br><span class="line">+ <span class="keyword">const</span> [counterState, counterDispatchers] = store.useModel(<span class="string">'counter'</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;&gt;&#123;counterState.count&#125;&lt;<span class="regexp">/&gt;</span></span><br><span class="line"><span class="regexp">  )</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<h4 id="API"><a href="#API" class="headerlink" title="API"></a>API</h4><p>createStore</p>
<p>该方法用于创建 Store。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">createStore(models, options);</span><br></pre></td></tr></table></figure>

<p><strong>options</strong></p>
<ul>
<li>disableImmer: 布尔值, 可选, 默认值 false，如果将此设置为true，那么 immer 将被禁用，这意味着您不能再在 reducers 中直接改变状态，而是必须返回新的状态。</li>
<li>disableError: 布尔值, 可选, 默认值 false，如果将此设置为 true，则 <code>UseModelEffectsError</code> 和 <code>WithModelEffectsError</code> 将不可用，仅当您非常关注性能或故意抛出错误时才启用该选项。</li>
<li>disableLoading: 布尔值, 可选, 默认值 false，如果将此设置为 true，则 <code>useModelEffectsLoading</code> 和 <code>withModelEffectsLoading</code> 将不可用。</li>
</ul>
<p>useModelState</p>
<p>通过该 hooks 使用模型的状态并订阅其更新。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">'@/store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">FunctionComponent</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> state = store.useModelState(<span class="string">'counter'</span>);</span><br><span class="line">  <span class="built_in">console</span>.log(state.value);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>useModelDispatchers</p>
<p>仅使用 Action 不使用 State。有些时候组件中只需要触发 action 不需要依赖对应的数据状态，此时可以使用 <code>useModelDispatchers</code> API。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">'@/store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">FunctionComponent</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> dispatchers = store.useModelDispatchers(<span class="string">'counter'</span>);</span><br><span class="line">  <span class="comment">// 只调用 increment 方法</span></span><br><span class="line">  dispatchers.increment();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>useModelEffectsState</p>
<p>异步 Action 状态。通过 <code>useModelEffectsState</code> API 即可获取到异步请求的 loading 和 error 状态。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">'@/store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">FunctionComponent</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [state, dispatchers] = store.useModel(<span class="string">'counter'</span>);</span><br><span class="line">  <span class="keyword">const</span> effectsState = store.useModelEffectsState(<span class="string">'counter'</span>);</span><br><span class="line"></span><br><span class="line">  useEffect(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    dispatchers.asyncDecrement();</span><br><span class="line">  &#125;, []);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// loading</span></span><br><span class="line">  <span class="built_in">console</span>.log(effectsState.asyncDecrement.isLoading);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// error</span></span><br><span class="line">  <span class="built_in">console</span>.log(effectsState.asyncDecrement.error);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>withModel</p>
<p>在 Class Component 中使用。useModel 相关的 API 基于 React 的 Hooks 能力，仅能在 Function Component 中使用，通过 <code>withModel</code> API 可以实现在 Class Component 中使用。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">'@/store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TodoList</span> <span class="keyword">extends</span> <span class="title">React</span>.<span class="title">Component</span> </span>&#123;</span><br><span class="line">  render() &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; todos &#125; = <span class="keyword">this</span>.props;</span><br><span class="line">    <span class="keyword">const</span> [ state, dispatchers ] = todos;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> store.withModel(<span class="string">'todos'</span>)(TodoList);</span><br><span class="line"><span class="comment">// 绑定多个 model</span></span><br><span class="line"><span class="comment">// export default withModel('user')(withModel('todos')(TodoList));</span></span><br></pre></td></tr></table></figure>

<p>同时，也可以使用 <code>withModelDispatchers</code> 以及 <code>withModelEffectsState</code> API。</p>
<h4 id="类型提示"><a href="#类型提示" class="headerlink" title="类型提示"></a>类型提示</h4><p>编写类型有助于更好的代码提示，类型定义步骤如下：</p>
<h5 id="创建-Store-实例时"><a href="#创建-Store-实例时" class="headerlink" title="创建 Store 实例时"></a>创建 Store 实例时</h5><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/pages/Dashboard/store.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; createStore, IStoreModels, IStoreDispatch, IStoreRootState &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="keyword">import</span> counter <span class="keyword">from</span> <span class="string">'./models/counter'</span>;</span><br><span class="line"></span><br><span class="line">+interface IAppStoreModels extends IStoreModels &#123;</span><br><span class="line">+  counter: <span class="keyword">typeof</span> counter;</span><br><span class="line">+&#125;;</span><br><span class="line"></span><br><span class="line">+<span class="keyword">const</span> models: IAppStoreModels = &#123;</span><br><span class="line">+  counter</span><br><span class="line">+&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> createStore(models);</span><br><span class="line"></span><br><span class="line">+<span class="comment">// 导出 IRootDispatch 类型</span></span><br><span class="line">+<span class="keyword">export</span> type IRootDispatch = IStoreDispatch&lt;<span class="keyword">typeof</span> models&gt;;</span><br><span class="line"></span><br><span class="line">+<span class="comment">// 导出 IRootState 类型</span></span><br><span class="line">+<span class="keyword">export</span> type IRootState = IStoreRootState&lt;<span class="keyword">typeof</span> models&gt;;</span><br></pre></td></tr></table></figure>

<h5 id="定义-Model-时"><a href="#定义-Model-时" class="headerlink" title="定义 Model 时"></a>定义 Model 时</h5><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/pages/Dashboard/models/counter.ts</span></span><br><span class="line">+<span class="keyword">import</span> &#123; IRootState, IRootDispatch &#125; <span class="keyword">from</span> <span class="string">'@/store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> user = &#123;</span><br><span class="line">  state: [],</span><br><span class="line">  reducers: &#123;&#125;,</span><br><span class="line">+ effects: (dispatch: <span class="function"><span class="params">IRootDispatch</span> =&gt;</span> (&#123;</span><br><span class="line">+   like(playload, <span class="attr">rootState</span>: IRootState) &#123;</span><br><span class="line"></span><br><span class="line">+   &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h3 id="静态构建"><a href="#静态构建" class="headerlink" title="静态构建"></a>静态构建</h3><p>静态构建是指在构建过程中，将页面内容提前构建到 HTML 里，静态构建与 SSR 的执行流程类似，只是将 SSR 的渲染时机提前到静态构建时。</p>
<p>哪些场景下适合静态构建</p>
<p>常见的不依赖于动态数据的页面，比如：官网、产品介绍页、规则说明页等等，这类静态页面都适合使用静态构建来避免不必要的 CSR 渲染过程。</p>
<p>对于普通的页面，如果页面不是完全依赖动态数据产生（比如商品列表），可以将页面的框架结构提前构建为 HTML 从而提升页面访问的体感。</p>
<p>如何开启静态构建</p>
<p>在 <code>build.json</code> 中，设置 <code>staticExport</code> 为 <code>true</code> 以开启静态构建能力。</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"targets"</span>: [<span class="string">"web"</span>],</span><br><span class="line">  <span class="attr">"web"</span>: &#123;</span><br><span class="line">    <span class="attr">"mpa"</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">"staticExport"</span>: <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="快照"><a href="#快照" class="headerlink" title="快照"></a>快照</h3><p>在传统的 CSR 渲染链路中，开发者的 js bundle 未执行前页面表现为白屏。在弱网条件下，白屏时间过长会导致用户体验下降。快照功能会将 HTML 内容缓存到 localStorage 中，从而在用户二次访问时极速展示页面内容，提升<strong>二次访问</strong>时的首屏加载速度。</p>
<p>在 <code>build.json</code> 中一键开启：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"targets"</span>: [<span class="string">"web"</span>],</span><br><span class="line">  <span class="attr">"web"</span>: &#123;</span><br><span class="line">    <span class="attr">"mpa"</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">"snapshot"</span>: <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="单页应用"><a href="#单页应用" class="headerlink" title="单页应用"></a>单页应用</h2><h3 id="应用入口"><a href="#应用入口" class="headerlink" title="应用入口"></a>应用入口</h3><p>对于 SPA 应用或者小程序应用，<code>src/app.tsx</code> 即整个应用的入口，通过 <code>runApp()</code> 运行应用：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line">runApp(&#123;&#125;);</span><br></pre></td></tr></table></figure>

<h4 id="配置项"><a href="#配置项" class="headerlink" title="配置项"></a>配置项</h4><p><code>runApp()</code> 接受一个对象作为配置参数，完整的配置参数如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">  app: &#123;</span><br><span class="line">    <span class="comment">// 可选，自定义添加 Provider</span></span><br><span class="line">    addProvider: <span class="function">(<span class="params">&#123; children &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">ConfigProvider</span>&gt;</span>&#123;children&#125;<span class="tag">&lt;/<span class="name">ConfigProvider</span>&gt;</span></span>;</span><br><span class="line">    &#125;,</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 可选，常用于 SSR 场景或者异步获取数据请求的场景</span></span><br><span class="line">    <span class="comment">// 如果返回字段中包含 initialStates 字段将会作为状态管理 store 的初始值</span></span><br><span class="line">    getInitialData: <span class="keyword">async</span>() =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> result = <span class="keyword">await</span> fetch();</span><br><span class="line">      <span class="keyword">return</span> result;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 可选，开启默认的 ErrorBoundary 行为，默认值为 false</span></span><br><span class="line">    errorBoundary: <span class="literal">true</span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 可选，自定义错误边界的 fallback UI</span></span><br><span class="line">    ErrorBoundaryFallback: <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>渲染错误<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 可选，自定义错误的处理事件</span></span><br><span class="line">    onErrorBoundaryHandler: (error, componentStack) &#123;</span><br><span class="line">      <span class="comment">// Do something with the error</span></span><br><span class="line">    &#125;,</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 可选，应用生命周期</span></span><br><span class="line">    onShow() &#123;&#125;,</span><br><span class="line">    onLaunch() &#123;&#125;,</span><br><span class="line">    onHide() &#123;&#125;,</span><br><span class="line">    onError() &#123;&#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">  router: &#123;</span><br><span class="line">    <span class="comment">// 默认值 hash，可选值 browser|hash|static</span></span><br><span class="line">    type: <span class="string">'browser'</span>,</span><br><span class="line">    <span class="comment">// 路由基准地址</span></span><br><span class="line">    basename: <span class="string">'/seller'</span>,</span><br><span class="line">    <span class="comment">// 加载页面 bundle 时的 fallback UI</span></span><br><span class="line">    fallback: <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>loading...<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>,</span><br><span class="line">    <span class="comment">// 自定义创建 history 对象，详见 https://github.com/ReactTraining/history/blob/master/</span></span><br><span class="line">    <span class="comment">// history</span></span><br><span class="line">  &#125;,</span><br><span class="line">  store: &#123;</span><br><span class="line">    initialStates: &#123;&#125;,</span><br><span class="line">    getInitialStates: <span class="function">(<span class="params">initialData</span>) =&gt;</span> &#123;&#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">runApp(appConfig);</span><br></pre></td></tr></table></figure>

<blockquote>
<p>当在 <code>app.ts</code> 中包含 <code>JSX</code> 元素的时候，需要将 <code>app.ts</code> 更名为 <code>app.tsx</code>。</p>
</blockquote>
<p>getInitialData:</p>
<p>通过 <code>app.getInitialData()</code> 可以在应用启动前获取一些异步状态，比如登录态判断、用户信息获取等。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">+  app: &#123;</span><br><span class="line">+    getInitialData: <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">+      <span class="comment">// const data = await fetch('/api/data');</span></span><br><span class="line">+      <span class="keyword">return</span> &#123; <span class="attr">userId</span>: <span class="string">'123'</span> &#125;;</span><br><span class="line">+    &#125;</span><br><span class="line">+  &#125;,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">runApp(appConfig);</span><br></pre></td></tr></table></figure>

<p>对于获取到的初始数据，可以通过 <code>getInitialData</code> API 在任何地方消费：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line">+<span class="keyword">import</span> &#123; getInitialData &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">+  <span class="keyword">const</span> initialData = getInitialData();</span><br><span class="line">  <span class="built_in">console</span>.log(initialData.userId);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h3 id="路由方案"><a href="#路由方案" class="headerlink" title="路由方案"></a>路由方案</h3><h4 id="路由配置"><a href="#路由配置" class="headerlink" title="路由配置"></a>路由配置</h4><p>在 <code>src/app.tsx</code> 中，我们可以配置路由的类型和基础路径等路由信息，具体配置如下：</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">  router: &#123;</span><br><span class="line">    type: <span class="string">'browser'</span>,</span><br><span class="line">    basename: <span class="string">'/seller'</span>,</span><br><span class="line">    fallback: <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>loading...<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">runApp(appConfig);</span><br></pre></td></tr></table></figure>

<h4 id="页面路由"><a href="#页面路由" class="headerlink" title="页面路由"></a>页面路由</h4><p>对于单页应用，通过 <code>src/app.json</code> 中的 <code>routes</code> 用于指定应用的页面：</p>
<ul>
<li><code>path</code>: 指定页面对应的路由地址</li>
<li><code>source</code>: 指定页面组件地址，必须写成 <code>pages/[PAGE_NAME]/index</code> 格式，暂不支持嵌套式路由</li>
<li><code>targets</code> 指定页面需要构建的端，默认为 <code>build.json</code> 所配置的 <code>targets</code> 的值</li>
<li><code>window</code>: 指定该页面的窗体表现，可以覆盖全局的窗口设置</li>
</ul>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [                    </span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/about"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/About/index"</span>,</span><br><span class="line">      <span class="attr">"targets"</span>: [<span class="string">"web"</span>]</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<blockquote>
<p>框架默认开启路由切割，每个页面会打包出一个独立 bundle</p>
</blockquote>
<h4 id="window-1"><a href="#window-1" class="headerlink" title="window"></a>window</h4><p>可以设置应用的窗口表现，同时也支持针对每个页面设置窗口表现。目前已经支持的参数的有：</p>
<table>
<thead>
<tr>
<th align="left">属性</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody><tr>
<td align="left">title</td>
<td align="left">页面标题</td>
</tr>
</tbody></table>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [                    </span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/about"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/About/index"</span>,</span><br><span class="line">      <span class="attr">"window"</span>: &#123;</span><br><span class="line">        <span class="attr">"title"</span>: <span class="string">"关于 Rax"</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">"window"</span>: &#123;</span><br><span class="line">    <span class="attr">"title"</span>: <span class="string">"应用默认 title"</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="tabBar-1"><a href="#tabBar-1" class="headerlink" title="tabBar"></a>tabBar</h4><p>如果你的应用是一个多 tab 应用（底部栏可以切换页面），那么可以通过 <code>tabBar</code> 配置项指定 tab 栏的表现，以及 tab 切换时显示的对应页面。</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span></span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">"tabBar"</span>: &#123;</span><br><span class="line">    <span class="attr">"textColor"</span>: <span class="string">"#999"</span>,</span><br><span class="line">    <span class="attr">"selectedColor"</span>: <span class="string">"#666"</span>,</span><br><span class="line">    <span class="attr">"backgroundColor"</span>: <span class="string">"#f8f8f8"</span>,</span><br><span class="line">    <span class="attr">"items"</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">"text"</span>: <span class="string">"home"</span>,</span><br><span class="line">        <span class="attr">"pageName"</span>: <span class="string">"/"</span>,</span><br><span class="line">        <span class="attr">"icon"</span>: <span class="string">"https://gw.alicdn.com/tfs/TB1ypSMTcfpK1RjSZFOXXa6nFXa-144-144.png"</span>,</span><br><span class="line">        <span class="attr">"activeIcon"</span>: <span class="string">"https://gw.alicdn.com/tfs/TB1NBiCTgHqK1RjSZFPXXcwapXa-144-144.png"</span></span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>tabBar 配置项如下：</p>
<table>
<thead>
<tr>
<th align="left">属性</th>
<th align="left">类型</th>
<th align="left">是否必填</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody><tr>
<td align="left">textColor</td>
<td align="left">HexColor</td>
<td align="left">否</td>
<td align="left">文字颜色</td>
</tr>
<tr>
<td align="left">selectedColor</td>
<td align="left">HexColor</td>
<td align="left">否</td>
<td align="left">选中文字颜色</td>
</tr>
<tr>
<td align="left">backgroundColor</td>
<td align="left">HexColor</td>
<td align="left">否</td>
<td align="left">背景色</td>
</tr>
<tr>
<td align="left">items</td>
<td align="left">Array</td>
<td align="left">是</td>
<td align="left">每个 tab 配置</td>
</tr>
</tbody></table>
<p>tab item 配置项如下：</p>
<table>
<thead>
<tr>
<th align="left">属性</th>
<th align="left">类型</th>
<th align="left">必填</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody><tr>
<td align="left">pageName</td>
<td align="left">String</td>
<td align="left">是</td>
<td align="left">设置页面路径，值为 <code>routes</code> 中配置的路由</td>
</tr>
<tr>
<td align="left">text</td>
<td align="left">String</td>
<td align="left">是</td>
<td align="left">tab item 上显示的文本</td>
</tr>
<tr>
<td align="left">icon</td>
<td align="left">String</td>
<td align="left">否</td>
<td align="left">非选中状态图标路径</td>
</tr>
<tr>
<td align="left">activeIcon</td>
<td align="left">String</td>
<td align="left">否</td>
<td align="left">选中状态图标路径</td>
</tr>
</tbody></table>
<h4 id="路由跳转"><a href="#路由跳转" class="headerlink" title="路由跳转"></a>路由跳转</h4><p>可以通过 <code>history</code> 获取路由实例。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; history &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于获取 history 跳转的动作，包含 PUSH、REPLACE 和 POP 三种类型</span></span><br><span class="line"><span class="built_in">console</span>.log(history.action);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于获取 location 对象，包含 pathname、search 和 hash</span></span><br><span class="line"><span class="built_in">console</span>.log(history.location);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于路由跳转</span></span><br><span class="line">history.push(<span class="string">'/home'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于路由替换</span></span><br><span class="line">history.replace(<span class="string">'/home'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于跳转到上一个路由</span></span><br><span class="line">history.goBack();</span><br></pre></td></tr></table></figure>

<p>更多 <a href="https://github.com/ReactTraining/history/blob/master/docs/GettingStarted.md" target="_blank" rel="noopener">history API</a></p>
<h4 id="路由参数获取"><a href="#路由参数获取" class="headerlink" title="路由参数获取"></a>路由参数获取</h4><p>可以通过 <code>getSearchParams</code> 获取路由参数。假设当前 URL 为 <code>https://example.com?foo=bar</code>，解析查询参数如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/components/Example</span></span><br><span class="line"><span class="keyword">import</span> &#123; getSearchParams &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Example</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> searchParams = getSearchParams()</span><br><span class="line">  <span class="comment">// console.log(searchParams); =&gt; &#123; foo: 'bar' &#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h4><p>HashHistory 与 BrowserHistory</p>
<p>前端路由通常有两种实现方式：HashHistory 和 BrowserHistory，路由都带着 <code>#</code> 说明使用的是 HashHistory。这两种方式优缺点：</p>
<table>
<thead>
<tr>
<th align="left">特点\方案</th>
<th align="left">HashRouter</th>
<th align="left">BrowserRouter</th>
</tr>
</thead>
<tbody><tr>
<td align="left">美观度</td>
<td align="left">不好，有 # 号</td>
<td align="left">好</td>
</tr>
<tr>
<td align="left">易用性</td>
<td align="left">简单</td>
<td align="left">中等，需要 server 配合</td>
</tr>
<tr>
<td align="left">依赖 server</td>
<td align="left">不依赖</td>
<td align="left">依赖</td>
</tr>
<tr>
<td align="left">跟锚点功能冲突</td>
<td align="left">冲突</td>
<td align="left">不冲突</td>
</tr>
<tr>
<td align="left">兼容性</td>
<td align="left">IE8</td>
<td align="left">IE10</td>
</tr>
</tbody></table>
<p>开发者可以根据自己的实际情况选择对应方案。</p>
<p>如何使用 BrowserRouter</p>
<p>本地开发时，只需要在 <code>src/app.tsx</code> 中增加以下配置即可：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">  router: &#123;</span><br><span class="line">+   type: <span class="string">'browser'</span>,</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">runApp(appConfig);</span><br></pre></td></tr></table></figure>

<p>线上运行时需要服务端支持，否则会出现刷新 404 问题，具体方案请参考社区文档：</p>
<h3 id="定制-HTML-结构-1"><a href="#定制-HTML-结构-1" class="headerlink" title="定制 HTML 结构"></a>定制 HTML 结构</h3><p>框架提供了两种方式帮助开发者定制 HTML 内容：</p>
<ol>
<li>推荐 通过 <code>app.json</code> 静态配置</li>
<li>不推荐 通过 <code>document/index.jsx</code> 灵活编写</li>
</ol>
<p>以下是两种方案的使用方式。</p>
<p>通过 app.json 定制</p>
<p>支持的配置项如下：</p>
<ul>
<li><code>metas</code>: 插入到 <code>&lt;head&gt;</code> 标签内</li>
<li><code>links</code>: 插入到业务样式之前</li>
<li><code>scripts</code>: 插入到业务脚本之前</li>
<li><code>window</code>: 配置页面标题</li>
</ul>
<p>具体使用方式如下：</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span>,</span><br><span class="line">+      "window": &#123;</span><br><span class="line">+        "title": "Home Page"</span><br><span class="line">+      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/about"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/About/index"</span>,</span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">+  // 插入 meta 标签</span><br><span class="line">+  "metas": [</span><br><span class="line">+    "&lt;meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\" /&gt;"</span><br><span class="line">+  ],</span><br><span class="line">+  // 插入 link 标签</span><br><span class="line">+  "links": [</span><br><span class="line">+    "&lt;link rel=\"dns-prefetch\" href=\"//g.alicdn.com\" /&gt;"</span><br><span class="line">+  ],</span><br><span class="line">+  // 插入 script 标签</span><br><span class="line">+  "scripts": [</span><br><span class="line">+    "&lt;script defer src=\"xxx/index.js\"&gt;&lt;/script&gt;"</span><br><span class="line">+  ],</span><br><span class="line">+  "window": &#123;</span><br><span class="line">+    // 设置整个应用的 title，优先级低于每个页面单独设置的 title</span><br><span class="line">+    "title": "Rax App"</span><br><span class="line">+  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>通过 Document 定制</p>
<p>同 MPA 文档定制。</p>
<h3 id="状态管理-1"><a href="#状态管理-1" class="headerlink" title="状态管理"></a>状态管理</h3><p>rax-app 内置了状态管理方案，并在此基础上进一步遵循 <strong>“约定优于配置”</strong> 原则，进行抽象和封装，使得状态管理变得非常容易。</p>
<p><strong>注意：</strong> 如果开发者没有依照约定的目录结构创建 <code>store</code>，框架将不会注入任何状态管理相关的能力。</p>
<h4 id="全局状态"><a href="#全局状态" class="headerlink" title="全局状态"></a>全局状态</h4><p>约定全局状态位于 <code>src/models</code> 目录，目录结构如下：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">src</span><br><span class="line">├── models               &#x2F;&#x2F; 全局状态</span><br><span class="line">|   ├── counter.ts</span><br><span class="line">│   └── user.ts</span><br><span class="line">├── app.tsx </span><br><span class="line">└── store.ts</span><br></pre></td></tr></table></figure>

<h4 id="定义模型"><a href="#定义模型" class="headerlink" title="定义模型"></a>定义模型</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/models/user.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> delay = <span class="function">(<span class="params">time</span>) =&gt;</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve</span>) =&gt;</span> setTimeout(<span class="function"><span class="params">()</span> =&gt;</span> resolve(), time));</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="comment">// 定义 model 的初始 state</span></span><br><span class="line">  state: &#123;</span><br><span class="line">    name: <span class="string">''</span>,</span><br><span class="line">    id: <span class="string">''</span></span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 定义改变该模型状态的纯函数</span></span><br><span class="line">  reducers: &#123;</span><br><span class="line">    update (prevState, payload) &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        ...prevState,</span><br><span class="line">        ...payload,</span><br><span class="line">      &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 定义处理该模型副作用的函数</span></span><br><span class="line">  effects: <span class="function">(<span class="params">dispatch</span>) =&gt;</span> (&#123;</span><br><span class="line">    <span class="keyword">async</span> updateUserInfo () &#123;</span><br><span class="line">      <span class="keyword">await</span> delay(<span class="number">1000</span>);</span><br><span class="line">      dispatch.user.update(&#123;</span><br><span class="line">        name: <span class="string">'taobao'</span>,</span><br><span class="line">        id: <span class="string">'123'</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;),</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h4 id="创建-Store-实例-1"><a href="#创建-Store-实例-1" class="headerlink" title="创建 Store 实例"></a>创建 Store 实例</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/store.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; createStore &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="keyword">import</span> user <span class="keyword">from</span> <span class="string">'./models/user'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> store = createStore(&#123; user &#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> store;</span><br></pre></td></tr></table></figure>

<h4 id="在-View-中使用状态"><a href="#在-View-中使用状态" class="headerlink" title="在 View 中使用状态"></a>在 View 中使用状态</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 引用全局状态</span></span><br><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">'@/store'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> HomePage = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> [userState, userDispatchers] = store.useModel(<span class="string">'user'</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;&gt;</span><br><span class="line">      &lt;span&gt;&#123;userState.id&#125;&lt;<span class="regexp">/span&gt;</span></span><br><span class="line"><span class="regexp">      &lt;span&gt;&#123;userState.name&#125;&lt;/</span>span&gt;</span><br><span class="line">    &lt;<span class="regexp">/&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<h4 id="模型规范-1"><a href="#模型规范-1" class="headerlink" title="模型规范"></a>模型规范</h4><p>参考 MPA 状态管理</p>
<h4 id="API-1"><a href="#API-1" class="headerlink" title="API"></a>API</h4><p>参考 MPA 状态管理</p>
<h4 id="类型提示-1"><a href="#类型提示-1" class="headerlink" title="类型提示"></a>类型提示</h4><p>参考 MPA 状态管理</p>
<h4 id="设置初始状态"><a href="#设置初始状态" class="headerlink" title="设置初始状态"></a>设置初始状态</h4><p>通过全局的 <code>store.initialStates</code> 即可同步设置整个应用的全局状态：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">  app: &#123;&#125;,</span><br><span class="line">  store: &#123;</span><br><span class="line">+    initialStates: &#123;</span><br><span class="line">+      <span class="comment">// 这里的 key 即对应 model 名称</span></span><br><span class="line">+      user: &#123;</span><br><span class="line">+        id: <span class="string">'123'</span></span><br><span class="line">+      &#125;</span><br><span class="line">+    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">runApp(appConfig);</span><br></pre></td></tr></table></figure>

<p>但是大多数情况下我们都需要异步设置全局状态，此时通过 <code>app.getInitialData()</code> 方法返回 <code>initialStates</code> 即可：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">  app: &#123;</span><br><span class="line">    getInitialData: <span class="keyword">async</span>() =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> result = <span class="keyword">await</span> fetch();</span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">+        initialStates: result.states</span><br><span class="line">      &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">runApp(appConfig);</span><br></pre></td></tr></table></figure>

<p>框架约定将 <code>app.getInitialData()</code> 返回的 <code>initialStates</code> 字段作为 store 的初始状态。如果不想使用该约定，可以通过 <code>store.getInitialStates</code> 更灵活的指定初始状态：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">  app: &#123;</span><br><span class="line">    getInitialData: <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123; <span class="attr">userId</span>: <span class="string">'123'</span> &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  store: &#123;</span><br><span class="line">+   <span class="comment">// 入参 initialData 即 getInitialData() 返回的数据</span></span><br><span class="line">+   getInitialStates: <span class="function">(<span class="params">initialData</span>) =&gt;</span> &#123;</span><br><span class="line">+     <span class="keyword">return</span> &#123;</span><br><span class="line">+       user: &#123; <span class="attr">id</span>: initialData.userId &#125;</span><br><span class="line">+     &#125;;</span><br><span class="line">+   &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h3 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h3><h4 id="应用生命周期"><a href="#应用生命周期" class="headerlink" title="应用生命周期"></a>应用生命周期</h4><table>
<thead>
<tr>
<th align="left"></th>
<th>行为表现</th>
</tr>
</thead>
<tbody><tr>
<td align="left">onShow</td>
<td>1. 应用唤起之后，在框架 loading 结束后触发 2. 应用从后台切到前台的时候触发</td>
</tr>
<tr>
<td align="left">onHide</td>
<td>应用从前台切到后台的时候触发</td>
</tr>
<tr>
<td align="left">onLaunch</td>
<td>1. 应用启动的时候触发 2. 传入参数为启动参数 (仅对小程序有效)</td>
</tr>
<tr>
<td align="left">onError</td>
<td>1. 监听全局 JS 报错 2. 传入参数为报错信息</td>
</tr>
</tbody></table>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">  app: &#123;</span><br><span class="line">    onShow() &#123;&#125;,</span><br><span class="line">    onLaunch() &#123;&#125;,</span><br><span class="line">    onHide() &#123;&#125;,</span><br><span class="line">    onError() &#123;&#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">runApp(appConfig);</span><br></pre></td></tr></table></figure>

<h4 id="页面生命周期"><a href="#页面生命周期" class="headerlink" title="页面生命周期"></a>页面生命周期</h4><table>
<thead>
<tr>
<th align="left"></th>
<th align="left">行为表现</th>
</tr>
</thead>
<tbody><tr>
<td align="left">onShow</td>
<td align="left">1. 切到目标页面时触发 2. 应用从后台切到前台的时候触发</td>
</tr>
<tr>
<td align="left">onHide</td>
<td align="left">1. 页面切走时触发 2. 页面卸载的时候触发</td>
</tr>
</tbody></table>
<h4 id="Hooks"><a href="#Hooks" class="headerlink" title="Hooks"></a>Hooks</h4><p>usePageShow</p>
<p>页面切换到前台且页面组件渲染后触发。</p>
<p>usePageHide</p>
<p>页面切换到后台后触发。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; usePageShow &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Home</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  usePageShow(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 首次调用，回调函数会在组件完成渲染后执行</span></span><br><span class="line">  &#125;);</span><br><span class="line">  usePageHide(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    </span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>Home<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Class Component</p>
<p>使用 <code>withPageLifeCyle</code> 给 Class Component 注入 <code>onShow</code>/<code>onHide</code> 属性。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement, Component &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; withPageLifeCycle &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Home</span> <span class="keyword">extends</span> <span class="title">Component</span> </span>&#123;</span><br><span class="line">  onShow() &#123;</span><br><span class="line">    <span class="comment">// 首次调用，函数在组件 constructor 之后调用</span></span><br><span class="line">  &#125;</span><br><span class="line">  render() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>Home<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> withPageLifeCycle(Home);</span><br></pre></td></tr></table></figure>

<h4 id="触发时序"><a href="#触发时序" class="headerlink" title="触发时序"></a>触发时序</h4><p><strong>启动阶段：</strong> app lanuch =&gt; app show =&gt; page show =&gt; component did mount <strong>页面切换(A =&gt; B)：</strong> page A hide =&gt; page B show <strong>页面返回(B =&gt; A)：</strong> page B hide =&gt; page A show <strong>应用前台切换到后台(A 页面)：</strong> page A hide =&gt; app hide <strong>应用后台切换到前台(A 页面)：</strong> app show =&gt; page A show</p>
<p>服务端渲染</p>
<p>框架支持服务端渲染（即 SSR）能力，开发者可以按需一键开启 SSR 的模式。</p>
<h3 id="开启-SSR"><a href="#开启-SSR" class="headerlink" title="开启 SSR"></a>开启 SSR</h3><p>在工程配置文件 <code>build.json</code> 中开启 SSR：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"web"</span>: &#123;</span><br><span class="line">    <span class="attr">"ssr"</span>: <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>配置完之后即可启用 SSR，同理置为 false 即可关闭 SSR 功能。开启后通过命令行 <code>npm run start</code> 即可启动调试服务，此时通过查看源代码发现返回的 HTML 已经是服务端渲染的节点结构：</p>
<p><img src="/Rax%E5%BA%94%E7%94%A8/O1CN01p9jhBd1bAEnwhpTPh_!!6000000003424-2-tps-2361-635.png" alt="SSR 效果"></p>
<h4 id="应用级数据"><a href="#应用级数据" class="headerlink" title="应用级数据"></a>应用级数据</h4><p>在 <code>src/app.tsx</code> 中可通过 <code>getInitialData</code> 获取全局数据：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; runApp &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> appConfig = &#123;</span><br><span class="line">+  app: &#123;</span><br><span class="line">+    getInitialData: <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">+      <span class="comment">// const data = await fetch('/api/data');</span></span><br><span class="line">+      <span class="keyword">return</span> &#123; <span class="attr">user</span>: &#123; <span class="attr">name</span>: <span class="string">'Jack Ma'</span>, <span class="attr">id</span>: <span class="string">'01'</span> &#125; &#125;</span><br><span class="line">+    &#125;</span><br><span class="line">+  &#125;,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">runApp(appConfig);</span><br></pre></td></tr></table></figure>

<p>开启了 SSR 的行为说明：</p>
<ul>
<li>服务端渲染时直接调用 <code>getInitialData</code> 获取数据并渲染应用，同时将数据注入到全局变量中</li>
<li>浏览器端渲染时不再调用 <code>getInitialData</code>，会直接通过全局变量获取初始数据</li>
<li>可以获取到当前请求的上下文 ctx 参数，包含以下字段<ul>
<li>ctx.req：HTTP request 对象 （仅在 server 端输出）</li>
<li>ctx.res：HTTP response 对象 （仅在 server 端输出）</li>
<li>ctx.pathname：当前路由路径</li>
<li>ctx.query：请求参数对象</li>
<li>ctx.path：URL 路径（包括请求参数）</li>
</ul>
</li>
</ul>
<p>未开启 SSR 的行为说明：</p>
<ul>
<li>浏览器端会同步调用 <code>getInitialData</code>，调用完成后执行 render 逻辑</li>
</ul>
<p>数据消费</p>
<p>通过 <code>getInitialData()</code> 获取的全局数据可以通过以下两种方式消费使用：</p>
<ul>
<li>全局 Store 消费：状态管理-设置初始状态</li>
<li>自定义消费：应用入口-getInitialData</li>
</ul>
<h4 id="页面级数据"><a href="#页面级数据" class="headerlink" title="页面级数据"></a>页面级数据</h4><p>在页面级组件中通过 <code>Component.getInitialProps</code> 来获取页面初始数据：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Home</span>(<span class="params">&#123; stars &#125;</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>rax stars: &#123;stars&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">+Home.getInitialProps = <span class="keyword">async</span> (ctx) =&gt; &#123;</span><br><span class="line">+  <span class="keyword">const</span> res = <span class="keyword">await</span> fetch(<span class="string">'https://github.com/alibaba/rax'</span>);</span><br><span class="line">+  <span class="keyword">return</span> &#123; <span class="attr">stars</span>: res.data.stargazers_count &#125;;</span><br><span class="line">+&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> Home;</span><br></pre></td></tr></table></figure>

<p>开启了 SSR 的行为说明：</p>
<ul>
<li>服务端渲染时调用对应页面的 <code>getInitialProps</code>，然后在渲染页面组件时将数据作为 props 传递给页面组件，同时将数据注入到全局变量上</li>
<li>浏览器端渲染时不再调用 <code>getInitialProps</code>，会直接通过全局变量获取初始数据并作为组件的 props</li>
<li>可以获取到当前请求的上下文 ctx 参数，包含以下字段<ul>
<li>ctx.req：HTTP request 对象 （仅在 server 端输出）</li>
<li>ctx.res：HTTP response 对象 （仅在 server 端输出）</li>
<li>ctx.pathname：当前路由路径</li>
<li>ctx.query：请求参数对象</li>
<li>ctx.path：URL 路径（包括请求参数）</li>
</ul>
</li>
</ul>
<p>未开启 SSR 的行为说明：</p>
<ul>
<li>浏览器端渲染时会在组件渲染前调用该方法，同时作为 props 传递给页面组件。</li>
</ul>

      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/20/%E7%9B%92%E9%A9%AC%E6%8A%80%E6%9C%AF%E6%A0%88%E5%88%9D%E5%AD%A6/Rax%E5%88%9D%E5%AD%A6/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/20/%E7%9B%92%E9%A9%AC%E6%8A%80%E6%9C%AF%E6%A0%88%E5%88%9D%E5%AD%A6/Rax%E5%88%9D%E5%AD%A6/" class="post-title-link" itemprop="url">Rax初学</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
    
              <time title="创建时间：2021-11-20 12:18:39" itemprop="dateCreated datePublished" datetime="2021-11-20T12:18:39+08:00">2021-11-20</time>
            </span>
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="far fa-calendar-check"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2021-11-24 15:56:15" itemprop="dateModified" datetime="2021-11-24T15:56:15+08:00">2021-11-24</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/%E7%9B%92%E9%A9%AC%E6%8A%80%E6%9C%AF%E6%A0%88/" itemprop="url" rel="index"><span itemprop="name">盒马技术栈</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Rax 是阿里巴巴应用最广泛的跨端解决方案，支持开发者通过类 React DSL 编写 Web、小程序、Flutter 等不同容器的跨端应用。</p>
<p>与Rax有关的官方体系如下：</p>
<p>Rax Core：支持多端的类React DSL；支持 80% 以上的 React API，比 React 更加轻量、渲染速度更快，同时通过 Driver 机制更好的支持跨多端渲染</p>
<p>Rax App：开箱即用的研发框架 Rax App；提供项目研发的基础规范和最佳实践，包含目录规范、工程能力、MPA、SPA、SSR、状态管理、路由、埋点监控等能力。</p>
<p>Rax Miniapp：跨端小程序；使用完整的 Rax 语法编写跨多厂商的小程序，以运行时为主，局部支持编写编译时组件。</p>
<p>Rax Components：跨多端的基础元件；抹平多端差异的基础元件，比如 Text、View、Image 等。</p>
<p>Fusion Mobile：丰富的跨端组件；支持 Web 以及小程序，不支持 Weex、Kraken</p>
<p>Uni API：跨多端的 API；支持微信、支付宝等厂商小程序，部分 API 支持 Web，暂不支持 App 里的各种 jsbridge。</p>
<h2 id="与React区别"><a href="#与React区别" class="headerlink" title="与React区别"></a>与React区别</h2><p>Rax与React的区别：1、Rax 面向多端设计的，从最初始就引入了 Driver 机制来适配不同端，这在 React 的早期版本中是不支持的；2、相比 React 更加轻量，gzip 之后只有 6KB。</p>
<p>针对React的诸多API，Rax不支持 Suspense、lazy API，其他诸如 Hooks、Component 等 API 都支持。</p>
<p>Rax 在 DSL 层面与框架设计方面借鉴了 React 的部分思想，但是舍弃了 React 部分传统的 API，所以体积上更加轻量。Rax 推荐开发者使用函数组件（Function Component）配合 Hooks 开发，相比传统类组件（Class Components）是一种更轻量便捷的开发风格。</p>
<p>如果我们的旧项目是用React框架实现的，我们想要把React项目代码迁移为Rax代码，则可以通过以下的几种方法来实现：</p>
<h3 id="修改写法迁移"><a href="#修改写法迁移" class="headerlink" title="修改写法迁移"></a>修改写法迁移</h3><p>由于底层实现不同，所以Rax项目和React项目引入的依赖是不同的。</p>
<p>创建Rax元素：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//在React中</span></span><br><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="comment">//在Rax中对应的修改</span></span><br><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br></pre></td></tr></table></figure>

<p>元素渲染：</p>
<p>在 React 中，需要在项目入口调用 <code>ReactDOM.render()</code>，将 <code>React Element</code> 渲染到真实的 DOM 节点。Rax 与之不同的是，Rax 支持跨容器渲染，在 Rax 的项目中调用 render 方法的时候，我们需要指定对应容器使用的 <a href="https://github.com/alibaba/rax/wiki/Driver-Specification" target="_blank" rel="noopener">Driver</a> 。基于此，我们需要做出以下对应的修改：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//在React中</span></span><br><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> ReactDOM <span class="keyword">from</span> <span class="string">'react-dom'</span>;</span><br><span class="line"></span><br><span class="line">ReactDOM.render(</span><br><span class="line">  &lt;h1&gt;Hello, world!&lt;<span class="regexp">/h1&gt;,</span></span><br><span class="line"><span class="regexp">  document.getElementById('root')</span></span><br><span class="line"><span class="regexp">);</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">/</span><span class="regexp">/在Rax中</span></span><br><span class="line"><span class="regexp">import &#123; createElement, render &#125; from 'rax';</span></span><br><span class="line"><span class="regexp">import * as DriverDOM from 'driver-dom';</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">render(</span></span><br><span class="line"><span class="regexp">  &lt;h1&gt;Hello, world!&lt;/</span>h1&gt;,</span><br><span class="line">  <span class="built_in">document</span>.body,</span><br><span class="line">  &#123; <span class="attr">driver</span>: DriverDOM &#125;</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<p>Webpack配置迁移：</p>
<p>在 Webpack 配置中，通过添加 react 和 react-dom 的 alias 来让使用 Rax 运行已有的 React 项目，这种方法不需要修改任何项目代码。</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  resolve: &#123;</span><br><span class="line">    alias: &#123;</span><br><span class="line">      'react': 'rax/lib/compat',</span><br><span class="line">      'react-dom': 'rax-dom'</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>通过工程插件迁移</p>
<p>如果使用的是 Rax 官方的工程，可以通过添加工程插件 <code>build-plugin-rax-compat-react</code> 的方式实现兼容，无需修改代码。详见<a href="https://www.npmjs.com/package/build-plugin-rax-compat-react" target="_blank" rel="noopener">build-plugin-rax-compat-react</a>。在 <code>build.json</code> 中按照以下方式配置该插件即可：</p>
<p>Rax cli 在初始化项目时同样提供了兼容 React 的选项，可以在 <code>build.json</code> 中自动生成上述配置</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"plugins"</span>: [</span><br><span class="line">    <span class="string">"build-plugin-rax-compat-react"</span></span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>为了减小 Rax 的体积，让开发者更加专注与 Hooks 的开发方式，Rax 将部分 API 从 Core 中移了出来。但我们仍然可以通过引入独立包的方式使用它们，以 <code>React.findDOMNode</code> 为例：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//在React中</span></span><br><span class="line">ReactDOM.findDOMNode(component);</span><br><span class="line"></span><br><span class="line"><span class="comment">//在Rax中</span></span><br><span class="line"><span class="keyword">import</span> findDOMNode <span class="keyword">from</span> <span class="string">'rax-find-dom-node'</span>;</span><br><span class="line">findDOMNode(component);</span><br></pre></td></tr></table></figure>

<h1 id="Rax基础指南"><a href="#Rax基础指南" class="headerlink" title="Rax基础指南"></a>Rax基础指南</h1><h2 id="组件编写"><a href="#组件编写" class="headerlink" title="组件编写"></a>组件编写</h2><h3 id="Component"><a href="#Component" class="headerlink" title="Component"></a>Component</h3><p>组件，在概念上类似于 Javascript 函数。它接受从父级元素传入的数据（即 Props)，并返回用于描述页面展示内容的 Rax 元素。组件使得我们的UI界面分成独立的、可重用的部分，并且，每个部分可以单独维护。</p>
<p>定义组件有两种方式，分别为 Function Component, 和 Class Component。</p>
<p>Function Component 是定义组件最简单的方式，这种方式会让代码变得更加简洁，并且可以使用 Hooks 编写更易维护和扩展的组件。我们推荐用 Function Component 来定义组件：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Welcome</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello, &#123;props.name&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//下面是ES6的Class编写class组件的写法</span></span><br><span class="line"><span class="keyword">import</span> &#123; createElement, Component &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Welcome</span> <span class="keyword">extends</span> <span class="title">Component</span> </span>&#123;</span><br><span class="line">  render() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello, &#123;this.props.name&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>需要注意的是，Rax 组件在命名时首字母必须大写。首字母为小写的组件会被视为原生 DOM 标签。例如，<code>&lt;div /&gt;</code> 代表 HTML 的 div 标签，而 <code>&lt;Welcome /&gt;</code> 则代表一个 Rax 组件。</p>
<h4 id="Props"><a href="#Props" class="headerlink" title="Props"></a>Props</h4><p>Props 为组件接受的入参，通过 Props, 组件可以接收来自父级节点的数据。Props 都是只读的，不能在当前组件中修改 Props 的值。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;Welcome name=<span class="string">"Tom"</span> /&gt;</span><br><span class="line">      &lt;Welcome name=<span class="string">"Lily"</span> /&gt;</span><br><span class="line">      &lt;Welcome name=<span class="string">"Susan"</span> /&gt;</span><br><span class="line">    &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">function Welcome(props) &#123;</span></span><br><span class="line"><span class="regexp">  return &lt;h1&gt;Hello, &#123;props.name&#125;&lt;/</span>h1&gt;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="State"><a href="#State" class="headerlink" title="State"></a>State</h4><p>State 是组件的私有变量，由组件自身控制，并与 Props 一起控制组件的渲染。我们使用 <code>useState</code> 这一 Hook 来进行定义和管理 State 。在下面的例子里，我们使用了 <code>useState</code> 来实现了一个计数器：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement, useState &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Example</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;p&gt;You clicked &#123;count&#125; times&lt;<span class="regexp">/p&gt;</span></span><br><span class="line"><span class="regexp">      &lt;button onClick=&#123;() =&gt; setCount(count + 1)&#125;&gt;</span></span><br><span class="line"><span class="regexp">        Click me</span></span><br><span class="line"><span class="regexp">      &lt;/</span>button&gt;</span><br><span class="line">    &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<p><code>useState</code> 参数是初始 state，会返回一个数组。数组第一个值是 state，第二个值是改变 state 的函数。上述例子中 count 与 setCount 是一一配对的，count 的值只能通过 setCount 修改。</p>
<h4 id="Fragment"><a href="#Fragment" class="headerlink" title="Fragment"></a>Fragment</h4><p>Fragment 是一个特殊组件，可以在不创建额外 DOM 元素的情况下，将多个元素组合在一起。在 rax 组件渲染时，若需要渲染多个元素，需要使用 Fragment 将元素包裹在内。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement, Fragment &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Hello</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;Fragment&gt;</span><br><span class="line">      &lt;h1&gt;Hello&lt;<span class="regexp">/h1&gt;</span></span><br><span class="line"><span class="regexp">      &lt;h1&gt;World&lt;/</span>h1&gt;</span><br><span class="line">    &lt;<span class="regexp">/Fragment&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<p>Fragment 组件只支持 <code>key</code> 属性 和 部分 JSX+ 属性即 <code>x-if</code> <code>x-for</code> <code>x-slot</code>，不支持其他属性。当 Fragment 组件不需要填写属性时，我们也可以使用<code>&lt;&gt;&lt;/&gt;</code>来代替。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Hello</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;&gt;</span><br><span class="line">      &lt;h1&gt;Hello&lt;<span class="regexp">/h1&gt;</span></span><br><span class="line"><span class="regexp">      &lt;h1&gt;World&lt;/</span>h1&gt;</span><br><span class="line">    &lt;<span class="regexp">/&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<h3 id="Hooks"><a href="#Hooks" class="headerlink" title="Hooks"></a>Hooks</h3><p>Hooks 是 Rax 1.0 新增的特性，它可以让函数组件（Function Component）使用状态和生命周期。Rax 在实现上遵循了 <a href="https://reactjs.org/docs/hooks-intro.html" target="_blank" rel="noopener">React Hooks</a> 的标准</p>
<h4 id="useState"><a href="#useState" class="headerlink" title="useState"></a>useState</h4><p>useState 主要用来定义和管理本地状态。在下面的例子里，我们使用了 <code>useState</code> 来实现了一个计数器</p>
<p><code>useState</code> 参数是初始 state，会返回一个数组。数组第一个值是 state，第二个值是改变 state 的函数。上述例子中 count 与 setCount 是一一配对的，count 的值只能通过 setCount 修改。</p>
<h4 id="useEffect"><a href="#useEffect" class="headerlink" title="useEffect"></a>useEffect</h4><p>useEffect 的代码在每次渲染后运行，包括第一次渲染：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement, useState, useEffect &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Example</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Similar to componentDidMount and componentDidUpdate:</span></span><br><span class="line">  useEffect(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">document</span>.title = <span class="string">`You clicked <span class="subst">$&#123;count&#125;</span> times`</span>;</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;p&gt;You clicked &#123;count&#125; times&lt;<span class="regexp">/p&gt;</span></span><br><span class="line"><span class="regexp">      &lt;button onClick=&#123;() =&gt; setCount(count + 1)&#125;&gt;</span></span><br><span class="line"><span class="regexp">        Click me</span></span><br><span class="line"><span class="regexp">      &lt;/</span>button&gt;</span><br><span class="line">    &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<p><code>useEffect</code> 它跟类组件（Class Component）中的 <code>componentDidMount</code>、<code>componentDidUpdate</code> 和 <code>componentWillUnmount</code> 具有相同的用途，只不过被合并成了一个 API。</p>
<h4 id="Hooks-使用规则"><a href="#Hooks-使用规则" class="headerlink" title="Hooks 使用规则"></a>Hooks 使用规则</h4><p>Hooks 就是 JavaScript 函数，但是使用它们会有两个额外的规则：</p>
<ul>
<li>只在<strong>函数最顶层</strong>调用 Hooks，不要在循环、条件判断或者子函数中调用。</li>
<li>只在 <strong>Rax 函数组件</strong> 和 <strong>自定义 Hooks</strong> 中调用 Hooks，不要在其他 JavaScript 函数中调用。</li>
</ul>
<p>其实各类规则与React Hooks的规则与注意事项一样，熟悉React Hooks即可。</p>
<h3 id="JSX-语法"><a href="#JSX-语法" class="headerlink" title="JSX+语法"></a>JSX+语法</h3><p>Rax 支持了一种 JSX 扩展语法 JSX+，它能帮助业务开发者更爽更快地书写 JSX。JSX+ 不是一种新的概念，它是 JSX 基础上的扩展指令概念。具体语法如下：</p>
<p>1、条件判断</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;View x-<span class="keyword">if</span>=&#123;condition&#125;&gt;Hello&lt;<span class="regexp">/View&gt;</span></span><br><span class="line"><span class="regexp">&lt;View x-elseif=&#123;anotherCondition&#125;&gt;&lt;/</span>View&gt;</span><br><span class="line">&lt;View x-<span class="keyword">else</span>&gt;NothingElse&lt;<span class="regexp">/View&gt;</span></span><br></pre></td></tr></table></figure>

<p>注： <code>x-elseif</code> 可以多次出现，但是顺序必须是 x-if -&gt; x-elseif -&gt; x-else，且这些节点是兄弟节点关系，如顺序错误则指令被忽略</p>
<p>2、循环列表</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&#123;<span class="comment">/* Array or Plain Object*/</span>&#125;</span><br><span class="line">&lt;tag x-<span class="keyword">for</span>=&#123;item <span class="keyword">in</span> foo&#125;&gt;&#123;item&#125;&lt;<span class="regexp">/tag&gt;</span></span><br><span class="line"><span class="regexp">&lt;tag x-for=&#123;(item, key) in foo&#125;&gt;&#123;key&#125;: &#123;item&#125;&lt;/</span>tag&gt;</span><br></pre></td></tr></table></figure>

<p>说明： 1. 若循环对象为数组，key 表示循环索引，其类型为 Number。 2. 当 <code>x-for</code> 与 <code>x-if</code> 同时作用在同一节点上时，循环优先级大于条件，即循环的 <code>item</code> 和 <code>index</code> 可以在子条件判断中使用。</p>
<p>3、单次渲染</p>
<p>仅在首次渲染时会触发 <code>createElement</code> 并将其引用缓存，re-render 时直接复用缓存，用于提高不带绑定节点渲染效率和 Diff 性能</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;p x-memo&gt;<span class="keyword">this</span> paragragh &#123;mesasge&#125; content will not change.&lt;<span class="regexp">/p&gt;</span></span><br></pre></td></tr></table></figure>

<p>4、插槽指令：类似于WebComponents 的 slot 概念，并提供插槽作用域。插槽用于父组件直接以插槽的形式，直接向子组件传递JSX元素，常用的有具名插槽与作用域插槽。</p>
<p>和Vue的插槽很像，在向具名插槽提供内容的时候，我们可以在一个 <code>&lt;view&gt;</code> 元素上使用 <code>x-slot</code> 指令，并以 <code>x-slot</code> 的参数的形式提供其名称；例如：header、footer。</p>
<p>作用域插槽：由于父级模板里的所有内容都是在父级作用域中编译的；子模板里的所有内容都是在子作用域中编译的。因此插槽中的信息无法访问子组件中数据，从而无法根据子组件数据更新而变化。因此有时让插槽内容能够访问子组件中才有的数据是很有用的。</p>
<p>如下例，为了让item在父级的插槽内容中可用，我们将item作为slot元素的一个属性绑定上去。绑定在slot元素上的属性被称为插槽prop，这样在父级作用域中，我们可以使用带值的 <code>x-slot</code> 来定义我们提供的插槽 prop 的名字：</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">&lt;tag x-slot:slotName=<span class="string">"slotScope"</span> /&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Example</span></span><br><span class="line">&lt;Waterfall&gt;</span><br><span class="line">  &lt;view x-slot:header&gt;header&lt;<span class="regexp">/view&gt;</span></span><br><span class="line"><span class="regexp">  &lt;view x-slot:item="props"&gt;&#123;props.index&#125;: &#123;props.item&#125;&lt;/</span>view&gt;</span><br><span class="line">  &lt;view x-slot:footer&gt;footer&lt;<span class="regexp">/view&gt;</span></span><br><span class="line"><span class="regexp">&lt;/</span>Waterfall&gt;</span><br><span class="line"></span><br><span class="line">&lt;slot name=<span class="string">"header"</span> /&gt; <span class="comment">// 槽位</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//对比传统的JSX如下：传递给子组件回调函数，在子组件中进行调用</span></span><br><span class="line">&lt;Waterfall</span><br><span class="line">  renderHeader=&#123;() =&gt; (<span class="xml"><span class="tag">&lt;<span class="name">view</span>&gt;</span>header<span class="tag">&lt;/<span class="name">view</span>&gt;</span></span>)&#125;</span><br><span class="line">  renderFooter=&#123;() =&gt; (<span class="xml"><span class="tag">&lt;<span class="name">view</span>&gt;</span>footer<span class="tag">&lt;/<span class="name">view</span>&gt;</span></span>)&#125;</span><br><span class="line">  renderItem=&#123;(item, index) =&gt; (<span class="xml"><span class="tag">&lt;<span class="name">view</span>&gt;</span>&#123;index&#125;: &#123;item&#125;<span class="tag">&lt;/<span class="name">view</span>&gt;</span></span>&#125;</span><br><span class="line">/&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment">//对比小程序的实现方式 slot-scope其实就是vue旧版本的实现方法。</span></span><br><span class="line">&lt;Waterfall&gt;</span><br><span class="line">  &lt;view slot=<span class="string">"header"</span>&gt;header&lt;<span class="regexp">/view&gt;</span></span><br><span class="line"><span class="regexp">  &lt;view slot="item" slot-scope="props"&gt;&#123;props.index&#125;: &#123;props.item&#125;&lt;/</span>view&gt;</span><br><span class="line">  &lt;view slot=<span class="string">"footer"</span>&gt;footer&lt;<span class="regexp">/view&gt;</span></span><br><span class="line"><span class="regexp">&lt;/</span>Waterfall&gt;</span><br></pre></td></tr></table></figure>

<p>5、Fragment组件</p>
<p>使用 <code>x-if</code> <code>x-for</code> <code>x-slot</code> 指令时，若不希望产生无意义的元素，我们可以使用 Fragment 组件。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;Fragment x-<span class="keyword">if</span>=&#123;condition&#125;&gt;</span><br><span class="line">  &lt;div /&gt;</span><br><span class="line">&lt;<span class="regexp">/Fragment&gt;</span></span><br></pre></td></tr></table></figure>

<p>6、类名绑定</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//语法</span></span><br><span class="line">&lt;div x-<span class="class"><span class="keyword">class</span></span>=&#123;&#123; <span class="attr">item</span>: <span class="literal">true</span>, <span class="attr">active</span>: val &#125;&#125; /&gt;</span><br><span class="line"><span class="comment">//参考示例如下：</span></span><br><span class="line">&lt;div className=&#123;classnames(&#123; <span class="attr">item</span>: <span class="literal">true</span>, <span class="attr">active</span>: val&#125;)&#125; /&gt;</span><br></pre></td></tr></table></figure>

<h4 id="JSX与HTML属性差异"><a href="#JSX与HTML属性差异" class="headerlink" title="JSX与HTML属性差异"></a>JSX与HTML属性差异</h4><p>className 属性用于指定 CSS 的 class，此特性适用于所有常规 DOM 节点和 SVG 元素，如 <code>&lt;div&gt;</code>，<code>&lt;a&gt;</code> 及其它标签。如果你在 JSX 中使用 Web Components，请使用 class 属性代替</p>
<p>dangerouslySetInnerHTML 是浏览器 DOM 提供 innerHTML 的替换方案。通常来讲，使用代码直接设置 HTML 存在风险，因为很容易无意中使用户暴露于跨站脚本（XSS）的攻击。因此，你可以直接在 JSX 中设置 HTML，但当你想设置 dangerouslySetInnerHTML 时，需要向其传递包含 key 为 <code>__html</code> 的对象，以此来警示你。例如：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createMarkup</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;<span class="attr">__html</span>: <span class="string">'First &amp;middot; Second'</span>&#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">MyComponent</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">dangerouslySetInnerHTML</span>=<span class="string">&#123;createMarkup()&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>由于 for 在 JavaScript 中是保留字，所以 JSX 元素中使用了 htmlFor 来代替。</p>
<h3 id="自定义Hooks"><a href="#自定义Hooks" class="headerlink" title="自定义Hooks"></a>自定义Hooks</h3><p>通过自定义Hooks能够将组件逻辑提取到可重用的函数中。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement, useState, useEffect &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> useDocumentTitle = <span class="function"><span class="keyword">function</span>(<span class="params">title</span>) </span>&#123;</span><br><span class="line">  useEffect(</span><br><span class="line">    () =&gt; &#123;</span><br><span class="line">      <span class="built_in">document</span>.title = title;</span><br><span class="line">    &#125;,</span><br><span class="line">    [title]</span><br><span class="line">  );</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Example</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = useState(<span class="number">0</span>);</span><br><span class="line">    useDocumentTitle(<span class="string">`You clicked <span class="subst">$&#123;count&#125;</span> times`</span>)</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;p&gt;You clicked &#123;count&#125; times&lt;<span class="regexp">/p&gt;</span></span><br><span class="line"><span class="regexp">      &lt;button onClick=&#123;() =&gt; setCount(count + 1)&#125;&gt;</span></span><br><span class="line"><span class="regexp">        Click me</span></span><br><span class="line"><span class="regexp">      &lt;/</span>button&gt;</span><br><span class="line">    &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<p>上述示例中 useDocumentTitle 是一个自定义 Hooks，可以在其他组件中执行设定 title 的逻辑。如果函数的名字以 “<code>use</code>” 开头并调用其他 Hooks，我们就说这是一个自定义 Hooks。</p>
<h4 id="Hooks与生命周期"><a href="#Hooks与生命周期" class="headerlink" title="Hooks与生命周期"></a>Hooks与生命周期</h4><p>通过以下 Hooks 实现对应的生命周期：</p>
<p>useEffect：1、componentDidUpdate；2、componentDidMount；3、componentWillUnmount。</p>
<p>useLayoutEffect：useEffect 一致，用于获取元素渲染结果。</p>
<p>useMemo、useCallback：shouldComponentUpdate。</p>
<p>使用 useEffect 完成 componentDidUpdate 的效果，示例：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//使用useEffect实现的生命周期示例如下：</span></span><br><span class="line"><span class="comment">//下面是使用 useEffect 完成 componentDidUpdate 的效果</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">DemoComponent</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  useEffect(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">p</span>&gt;</span>Hello hooks!<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//下面是使用useEffect实现componentDidMount 的功能</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">DemoComponent</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  useEffect(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// useEffect 的第二个参数为[]时，表示这个 effect 只会在 componentDidMount、componentWillUnMount 的时候调用。</span></span><br><span class="line">  &#125;, []);</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">p</span>&gt;</span>Hello hooks!<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//下面是使用useEffect实现componentWillUnmount 的功能，</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">DemoComponent</span>(<span class="params">&#123;source&#125;</span>) </span>&#123;</span><br><span class="line">  useEffect(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> subscription = source.subscribe();</span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// componentWillUnMount 调用的是第一个参数返回的回调。</span></span><br><span class="line">      <span class="comment">//(一个是调用函数本身，一个是调用useEffect函数返回的回调。)</span></span><br><span class="line">      subscription.unsubscribe();</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;, [source]); <span class="comment">// 表示 source 改变时就是执行一遍</span></span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">p</span>&gt;</span>Hello hooks!<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//使用memo可以实现shouldComponentUpdate 的功能</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">DemoComponent</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>I am &#123;props.name&#125;. hi~<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line">memo(DemoComponent, (prevProps, nextProps) =&gt; prevProps.name === nextProps.name);</span><br><span class="line"><span class="comment">// 添加第二个参数来指定采用旧 props 和新 props 的自定义比较函数。如果返回 true，则跳过更新。</span></span><br></pre></td></tr></table></figure>

<h2 id="样式方案"><a href="#样式方案" class="headerlink" title="样式方案"></a>样式方案</h2><h3 id="非内联样式CSS"><a href="#非内联样式CSS" class="headerlink" title="非内联样式CSS"></a>非内联样式CSS</h3><p>全局样式：对于整个项目的全局样式，推荐统一定义在<code>src/global.[css|less|scss]</code> 文件中，框架会自动引入该文件：</p>
<p>组件样式：对于页面级和组件级的样式，我们推荐使用 CSS Modules 的方案，这能很好的解决样式开发中全局污染以及命名冲突的问题。</p>
<p>具体规范规则如下：</p>
<ul>
<li>文件名：约定文件名格式为 <code>xxx.module.[css|less|scss]</code></li>
<li>模块化：一个页面或者一个组件对应一个样式文件</li>
</ul>
<p>Home ├── index.module.css └── index.tsx</p>
<p>首先编写样式文件内容；</p>
<figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/** ./pages/Home/index.module.css */</span></span><br><span class="line"><span class="selector-class">.container</span> &#123;</span><br><span class="line">  <span class="attribute">background</span>: <span class="number">#fff</span>;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">750</span>rpx;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 也可通过 CSS Modules 的 :global 语法定义全局样式 */</span></span><br><span class="line"><span class="selector-pseudo">:global</span> &#123;</span><br><span class="line">  body &#123;</span><br><span class="line">    <span class="selector-tag">a</span> &#123;</span><br><span class="line">      <span class="attribute">color</span>: blue;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>之后在文件中引入对应的样式文件，并将className与对应样式关联：</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ./pages/Home/index.tsx</span></span><br><span class="line"><span class="keyword">import</span> styles <span class="keyword">from</span> <span class="string">'./index.module.css'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Home</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;View className=&#123;styles.container&#125;&gt;</span><br><span class="line">      &lt;View&gt;CSS Modules&lt;<span class="regexp">/View&gt;</span></span><br><span class="line"><span class="regexp">    &lt;/</span>View&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">使用该方案之后，上文中的 className 都会被编译为唯一性的名字，避免因为重名 className 而产生样式冲突，如果在浏览器里查看这个示例的 dom 结构，你会发现实际渲染出来是这样的：</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">&lt;View <span class="class"><span class="keyword">class</span></span>=<span class="string">"container--1DTudAN"</span>&gt;title&lt;<span class="regexp">/View&gt;</span></span><br></pre></td></tr></table></figure>

<h3 id="内联样式"><a href="#内联样式" class="headerlink" title="内联样式"></a>内联样式</h3><p>如果在 <code>build.json</code> 里配置了 <code>inlineStyle: true</code> 则说明整个项目使用内联样式。对于 Weex、Kraken 这些暂不支持 CSS 的渲染引擎，只支持内联样式（即通过元素的 style 属性设置样式）。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> myStyle = &#123;</span><br><span class="line">  fontSize: <span class="string">'24px'</span>,</span><br><span class="line">  color: <span class="string">'#FF0000'</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> element = <span class="xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">style</span>=<span class="string">&#123;myStyle&#125;</span>&gt;</span>Hello Rax<span class="tag">&lt;/<span class="name">View</span>&gt;</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">同时也支持在 CSS 文件中编写样式，然后通过编译工具最终生成内联的样式，具体使用方式如下：</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">index.css</span></span><br><span class="line"><span class="comment">.container &#123;</span></span><br><span class="line"><span class="comment">  background: #f40;</span></span><br><span class="line"><span class="comment">  padding: 30rpx;</span></span><br><span class="line"><span class="comment">&#125;</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">'./index.css'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">Title</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> (<span class="xml"><span class="tag">&lt;<span class="name">View</span> <span class="attr">className</span>=<span class="string">"container"</span>&gt;</span>...<span class="tag">&lt;/<span class="name">View</span>&gt;</span></span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>同时，我们也支持在使用内联样式方案的项目中局部支持非内联形式，即强制将样式构建为内联时，忽略 <code>src/global.[css|less|scss]</code> 和 <code>*.module.[css|less|scss]</code>，将两者构建为单独的 CSS 文件，用于提供 <code>@keyframes</code> 等 CSS 能力。</p>
<p>在 <code>build.json</code> 中将 <code>inlineStyle</code> 设置为 <code>{ forceEnableCSS: true }</code> 会开启此能力。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//基本用法</span></span><br><span class="line"><span class="keyword">import</span> classNames <span class="keyword">from</span> <span class="string">'classnames'</span>;</span><br><span class="line"><span class="keyword">import</span> cssModule <span class="keyword">from</span> <span class="string">'./index.module.css'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// src/global.css 将会被自动引入并构建为单独的 CSS 文件</span></span><br><span class="line">  <span class="comment">// 在此处 'header' 将会引用 src/global.css 中的样式</span></span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&#123;classNames(cssModule.header,</span> '<span class="attr">header</span>')&#125; /&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//推荐用法</span></span><br><span class="line"><span class="keyword">import</span> cssModule <span class="keyword">from</span> <span class="string">'./index.module.css'</span>;</span><br><span class="line"><span class="keyword">import</span> styles <span class="keyword">from</span> <span class="string">'./index.css'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// CSS Modules 以 className 的形式使用</span></span><br><span class="line">  <span class="comment">// inlineStyle 以 style 的形式使用</span></span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&#123;cssModule.header&#125;</span> <span class="attr">style</span>=<span class="string">&#123;styles.header&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//不推荐用法</span></span><br><span class="line"><span class="keyword">import</span> classNames <span class="keyword">from</span> <span class="string">'classnames'</span>;</span><br><span class="line"><span class="keyword">import</span> cssModule <span class="keyword">from</span> <span class="string">'./index.module.css'</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">'./index.css'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="comment">// index.css 中的样式不会被内联</span></span><br><span class="line">  <span class="comment">// 在组件中使用了 CSS Module 时 index.css 将会失效</span></span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&#123;classNames(cssModule.header,</span> '<span class="attr">header</span>')&#125; /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="进阶指南"><a href="#进阶指南" class="headerlink" title="进阶指南"></a>进阶指南</h3><p>自适应单位rpx：Rax 采用 rpx(responsive pixel) 作为跨端的样式单位，它可以根据屏幕宽度进行自适应。我们规定屏幕宽度为 750rpx，以 iPhone6 为例，它的屏幕宽度为 375px，则 750rpx = 375px = 100vw，所以在 iPhone6 中，1rpx = 0.5px = 100/750vw。</p>
<p>从内联样式迁移到非内联样式：某些 Web 应用可能因为一些历史原因导致使用了内联样式的方案，对于此类应用我·们推荐迁移到非内联样式，这样可以使用更加强大的 CSS 能力</p>
<p>CSS预处理器：Rax App 默认支持了 Less 和 Sass 预处理器，你只需要按照 <code>.less</code> 或 <code>.scss</code> 的规则命名文件即可。如果使用 Sass 你还需要将 sass 的编译器比如社区推荐的 dart-sass：</p>
<p>如果在编辑器中遇到 ts 错误 <code>找不到模块“./index.module.css”或其相应的类型声明</code>，需要在 <code>src/</code> 下新建 <code>typings.d.ts</code> 文件：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/typings.d.ts</span></span><br><span class="line">declare <span class="built_in">module</span> <span class="string">'*.module.css'</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> classes: &#123; [key: string]: string &#125;;</span><br><span class="line">  <span class="keyword">export</span> <span class="keyword">default</span> classes;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Classname压缩细节：该部分是对实验属性 <code>experiments.minifyCSSModules</code> 的实现细节说明。</p>
<p>对于普通的 web 应用，我们使用了 <code>字母</code> + <code>hash</code> 的方式生成 classname，效果如下：</p>
<p>开启前，产物 CSS。</p>
<p>.QueryForm–input–2crH3Qq {} .QueryForm–button–6sdyaqk {}</p>
<p>开启后，产物 CSS</p>
<p>.a2715f34c {} .b2715f34c {}</p>
<p>仔细观察可以发现，上述 classname 中都包含了一个相同的 hash( <code>2715f34c</code> )，同时前面再拼接 a/b/c/…，该方案和 hash:base64:8 一个明显的不同是，我们产出的 classname 更利于 Gzip/Brotli 等算法的压缩，在 Gzip/Brotli 加持下，该策略对于纯 classname，压缩率可以达到 20%~30%（也就是减少 70-80%）。实际项目中由于除了 classname 外，还有样式代码，实际压缩率并不会达到这种程度，但是也可以减少 20% 左右（数据供参考，以实际使用效果为准）</p>
<p>而对于小程序场景，我们使用了另一种压缩策略，效果如下</p>
<p>开启前，产物 CSS</p>
<p>.QueryForm–input–2crH3Qq {} .QueryForm–button–6sdyaqk {}</p>
<p>开启后，产物 CSS</p>
<p>.a-a_mc {} .b-a_mc {}</p>
<p>相较于 Web 应用，在小程序中 hash( <code>2715f34c</code> ) 被替换成了 <code>a</code> 以获得更小的 CSS 体积，同时为了解决和三方 classname 冲突的问题，我们添加了统一的后缀 <code>_mc</code>，对于一些囿于小程序 2M 限制的应用，可以尝试开启该特性。</p>
<h1 id="Rax进阶指南"><a href="#Rax进阶指南" class="headerlink" title="Rax进阶指南"></a>Rax进阶指南</h1><h2 id="框架API"><a href="#框架API" class="headerlink" title="框架API"></a>框架API</h2><h3 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h3><p>rax-app 提供的 API 列表，通常情况下可以通过 <code>rax-app</code> 导入：</p>
<p>runApp：用于创建渲染小程序或 SPA 应用。</p>
<p>APP_MODE：获取应用环境。</p>
<p>ErrorBoundary：用于错误边界的组件。</p>
<p>IAppConfig：appConfig 的类型定义。</p>
<h3 id="状态管理"><a href="#状态管理" class="headerlink" title="状态管理"></a>状态管理</h3><p>可以通过在 build.json 中设置 <code>store: false</code> 关闭状态管理。</p>
<p>store：应用级别的store实例。</p>
<p>IStoreModels：定义模型的类型。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; IStoreModels &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line">interface IAppStoreModels extends IStoreModels &#123;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>IStoreDispatch:定义 Dispatch 的类型。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; IStoreDispatch &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="keyword">const</span> models = &#123;&#125;;</span><br><span class="line"><span class="keyword">export</span> type IRootDispatch = IStoreDispatch&lt;<span class="keyword">typeof</span> models&gt;;</span><br></pre></td></tr></table></figure>

<p>IStoreRootState：定义 RootState 的类型。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; IStoreRootState &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>; </span><br><span class="line"><span class="keyword">const</span> models = &#123;&#125;;</span><br><span class="line"><span class="keyword">export</span> type IRootState = IStoreRootState&lt;<span class="keyword">typeof</span> models&gt;;</span><br></pre></td></tr></table></figure>

<h3 id="路由"><a href="#路由" class="headerlink" title="路由"></a>路由</h3><p>getHistory：用于获取 history 实例。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; getHistory &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">HomeButton</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> history = getHistory();</span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">handleClick</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    history.push(<span class="string">'/home'</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;button type=<span class="string">'button'</span> onClick=&#123;handleClick&#125;&gt;</span><br><span class="line">      Go home</span><br><span class="line">    &lt;<span class="regexp">/button&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<p>getSearchParams：用于解析 url 参数。假设当前 URL 为 <code>https://example.com?foo=bar</code>，解析查询参数如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/components/Example</span></span><br><span class="line"><span class="keyword">import</span> &#123; getSearchParams &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Example</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> searchParams = getSearchParams()</span><br><span class="line">  <span class="comment">// console.log(searchParams); =&gt; &#123; foo: 'bar' &#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>withRouter：通过在 Class 组件上添加 <code>withRouter</code> 装饰器，可以获取到路由的 <code>history</code>、<code>location</code>、<code>match</code> 对象。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; withRouter &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">ShowTheLocation</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; history, location &#125; = props;</span><br><span class="line">  <span class="keyword">const</span> handleHistoryPush = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    history.push(<span class="string">'/new-path'</span>);</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;div&gt;当前路径： &#123;location.pathname&#125;&lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">      &lt;button onClick=&#123;handleHistoryPush&#125;&gt;点击跳转新页面&lt;/</span>button&gt;</span><br><span class="line">    &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">export default withRouter(ShowTheLocation);</span></span><br></pre></td></tr></table></figure>

<p>history：获取应用的路由实例</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; history &#125; <span class="keyword">from</span> <span class="string">'rax-app'</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于获取 history 栈里的实体个数</span></span><br><span class="line"><span class="built_in">console</span>.log(history.length);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于获取 history 跳转的动作，包含 PUSH、REPLACE 和 POP 三种类型</span></span><br><span class="line"><span class="built_in">console</span>.log(history.action);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于获取 location 对象，包含 pathname、search 和 hash</span></span><br><span class="line"><span class="built_in">console</span>.log(history.location);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于路由跳转</span></span><br><span class="line">history.push(<span class="string">'/home'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于路由替换</span></span><br><span class="line">history.replace(<span class="string">'/home'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于跳转到上一个路由</span></span><br><span class="line">history.goBack();</span><br></pre></td></tr></table></figure>

<h2 id="安全区域适配"><a href="#安全区域适配" class="headerlink" title="安全区域适配"></a>安全区域适配</h2><p>安全区域（Safe Area），指屏幕内不受圆角（corners）、刘海（sensor housing）、底部小黑条（Home Indicator）等元素影响的可视窗口。</p>
<p>对于移动端业务，特别是开启全屏模式的业务（隐藏了浏览器默认的导航栏），为了保证页面的核心内容、可操作区域是在安全区域内，常常需要做安全区域的适配，比如：</p>
<ul>
<li>根据刘海的高度，调整页面主内容距离顶部的高度</li>
<li>根据底部小黑条的高度，调整底部 Bar 的高度</li>
</ul>
<p>WebKit 从 iOS11 起引入一系列 API，来获取安全区域的位置。比如通过环境变量 <code>safe-area-inset-top</code> 可以获取安全区域距离视口顶部的距离，即刘海区域的高度；通过 <code>safe-area-inset-bottom</code> 可以获取安全区域距离视口底部的距离，即小黑条区域的高度。</p>
<h3 id="刘海屏"><a href="#刘海屏" class="headerlink" title="刘海屏"></a>刘海屏</h3><p>为了使页面顶部内容，不被刘海遮挡，可以通过设置容器节点的 padding-top 值来实现，使核心内容整体下移。</p>
<p>获取刘海高度，首先需要设置 <code>viewport-fit</code> ，调整可视窗口的布局方式。当且仅当 <code>viewport-fit</code> 设置为 <code>cover</code> 时，可以进一步设置页面的安全区域范围。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&lt;meta name=<span class="string">"viewport"</span> content=<span class="string">"width=device-width, viewport-fit=cover"</span>&gt;</span><br><span class="line"><span class="comment">//然后，结合 env() 方法，可以获取 safe-area-inset-top 值，并将其作为容器节点的 padding-top 值。</span></span><br><span class="line">.root &#123;</span><br><span class="line">  padding-top: constant(safe-area-inset-top); <span class="comment">/* 兼容  iOS &lt; 11.2 */</span></span><br><span class="line">  padding-top: env(safe-area-inset-top); <span class="comment">// /* iOS &gt; 11.2 */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="小黑条适配"><a href="#小黑条适配" class="headerlink" title="小黑条适配"></a>小黑条适配</h3><p>对于有 tabbar 的应用，iPhone 底部的小黑条常常会挡住 tabbar，影响其可操作区域。和刘海屏适配的原理一致，以 tabbar 为例，小黑条适配可以通过调整 tabbar 的 padding-bottom 值，增加空白区域来实现。</p>
<figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*通过设置 tabbar 的 padding 值来调整安全区域*/</span></span><br><span class="line"><span class="selector-class">.tabbar</span> &#123;</span><br><span class="line">  <span class="attribute">padding-bottom</span>: <span class="number">0</span>; <span class="comment">/* 无小黑条的情况下，无需额外设置  */</span></span><br><span class="line">  <span class="attribute">padding-bottom</span>: <span class="built_in">constant</span>(safe-area-inset-bottom); <span class="comment">/* 兼容  iOS &lt; 11.2 */</span></span><br><span class="line">  padding-bottom: env(safe-area-inset-bottom); // /* iOS &gt; 11.2 */</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="动画方案"><a href="#动画方案" class="headerlink" title="动画方案"></a>动画方案</h2><p>对于基础的动效，推荐的做法是结合 CSS 属性 transition 或 animation 实现。</p>
<h3 id="Transitions"><a href="#Transitions" class="headerlink" title="Transitions"></a>Transitions</h3><p>CSS <a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions" target="_blank" rel="noopener">transitions</a> 提供了一种在更改CSS属性时控制动画速度的方法。它可以让属性变化成为一个持续一段时间的过程，而不是立即生效的。比如，将一个元素的颜色从白色改为黑色，通常这个改变是立即生效的，使用 CSS transitions 后该元素的颜色将逐渐从白色变为黑色，按照一定的曲线速率变化，这个过程可以自定义。</p>
<p>示例：当 hover 到节点时，节点的宽高、背景色及 transform 值发生了渐变。</p>
<figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* index.module.css */</span></span><br><span class="line"><span class="selector-class">.box</span> &#123;</span><br><span class="line">  <span class="attribute">border-style</span>: solid;</span><br><span class="line">  <span class="attribute">border-width</span>: <span class="number">1px</span>;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">100</span>rpx;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">100</span>rpx;</span><br><span class="line">  <span class="attribute">background-color</span>: <span class="number">#0000FF</span>;</span><br><span class="line">  <span class="attribute">-webkit-transition</span>:width <span class="number">2s</span>, height <span class="number">2s</span>,</span><br><span class="line">      background-color <span class="number">2s</span>, -webkit-transform <span class="number">2s</span>;</span><br><span class="line">  <span class="attribute">transition</span>:width <span class="number">2s</span>, height <span class="number">2s</span>, background-color <span class="number">2s</span>, transform <span class="number">2s</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-class">.box</span><span class="selector-pseudo">:hover</span> &#123;</span><br><span class="line">  <span class="attribute">background-color</span>: <span class="number">#FFCCCC</span>;</span><br><span class="line">  <span class="attribute">width</span>:<span class="number">200</span>rpx;</span><br><span class="line">  <span class="attribute">height</span>:<span class="number">200</span>rpx;</span><br><span class="line">  <span class="attribute">-webkit-transform</span>:<span class="built_in">rotate</span>(<span class="number">180deg</span>);</span><br><span class="line">  <span class="attribute">transform</span>:<span class="built_in">rotate</span>(<span class="number">180deg</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// index.jsx</span></span><br><span class="line"><span class="keyword">import</span> styles <span class="keyword">from</span> <span class="string">'./index.module.css'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">className</span>=<span class="string">&#123;styles.box&#125;</span> /&gt;</span></span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h3 id="Animations"><a href="#Animations" class="headerlink" title="Animations"></a>Animations</h3><p>CSS <a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Animations/Using_CSS_animations" target="_blank" rel="noopener">animations</a> 提供了通过 <code>关键帧</code> 创建简单动画的能力。通过使用 @keyframes 指定动画开始、结束以及中间点的关键帧，在每一帧上描述了动画元素在给定的时间点上应该如何渲染（CSS样式配置）， 就能够非常容易地创建动画序列。</p>
<p>示例：通过 animation 实现节点显示时，从屏幕右侧划入的效果。</p>
<figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* index.module.css */</span></span><br><span class="line"><span class="selector-class">.box</span> &#123;</span><br><span class="line">  <span class="attribute">width</span>: <span class="number">100</span>rpx;</span><br><span class="line">  <span class="attribute">height</span>: <span class="number">100</span>rpx;</span><br><span class="line">  <span class="attribute">background-color</span>: <span class="number">#0000FF</span>;</span><br><span class="line">  <span class="attribute">animation-duration</span>: <span class="number">3s</span>;</span><br><span class="line">  <span class="attribute">animation-name</span>: slidein;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@keyframes</span> slidein &#123;</span><br><span class="line">  <span class="selector-tag">from</span> &#123;</span><br><span class="line">    <span class="attribute">margin-left</span>: <span class="number">100%</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="selector-tag">to</span> &#123;</span><br><span class="line">    <span class="attribute">margin-left</span>: <span class="number">0%</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// index.jsx</span></span><br><span class="line"><span class="keyword">import</span> &#123; createElement, useState &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> View <span class="keyword">from</span> <span class="string">'rax-view'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> styles <span class="keyword">from</span> <span class="string">'./index.module.css'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">Home</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [showBox, setBox] = useState(<span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">function</span> <span class="title">onClick</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    setBox(<span class="literal">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;View&gt;</span><br><span class="line">      &lt;View className=&#123;styles.buttom&#125; onClick=&#123;onClick&#125;&gt;show box&lt;<span class="regexp">/View&gt;</span></span><br><span class="line"><span class="regexp">      &#123;</span></span><br><span class="line"><span class="regexp">        showBox &amp;&amp; &lt;View className=&#123;styles.box&#125;&gt;&lt;/</span>View&gt;</span><br><span class="line">      &#125;</span><br><span class="line">    &lt;<span class="regexp">/View&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure>

<h3 id="静态资源"><a href="#静态资源" class="headerlink" title="静态资源"></a>静态资源</h3><p><code>src/app.json</code> 用于对应用进行全局配置，设置路由、窗口表现、渲染方式等。默认配置示例如下；可根据具体场景进行配置使用：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"routes"</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">"path"</span>: <span class="string">"/"</span>,</span><br><span class="line">      <span class="attr">"source"</span>: <span class="string">"pages/Home/index"</span></span><br><span class="line">    &#125;</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">"window"</span>: &#123;</span><br><span class="line">    <span class="attr">"title"</span>: <span class="string">"Rax App 1.0"</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="静态资源不构建"><a href="#静态资源不构建" class="headerlink" title="静态资源不构建"></a>静态资源不构建</h4><p>你可以将无需经过 webpack 构建的静态资源添加到 <code>public</code> 文件夹。</p>
<p>如果将文件放入 <code>public</code> 文件夹，webpack 将 <strong>不会</strong> 处理它。而是它将被复制到构建文件夹中。要引用 <code>public</code> 文件夹中的资源，需要使用名为 <code>process.env.PUBLIC_URL</code> 的特殊变量，这个值会根据工程配中的 <code>publicPath</code> 变化：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">render() &#123;</span><br><span class="line">  <span class="comment">// 注意：这是一个 escape hatch，应该谨慎使用！</span></span><br><span class="line">  <span class="comment">// 通常我们建议使用`import`来获取资源的 URL </span></span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&#123;process.env.PUBLIC_URL</span> + '/<span class="attr">img</span>/<span class="attr">logo.png</span>'&#125; /&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>注意这种方法的缺点：</p>
<ul>
<li><code>public</code> 文件夹中的所有文件都不会进行后处理或压缩</li>
<li>在编译时不会调用丢失的文件，并且会导致用户出现 404 错误</li>
<li>构建产物的文件名不包含内容哈希值，因此你需要添加查询参数或在每次更改时重命名它们（以便清除浏览器缓存）</li>
</ul>
<p>一般情况下，我们建议你在 JavaScript 文件中 <code>import</code> 资源，这种机制提供了许多好处：</p>
<ul>
<li>脚本和样式被压缩并打包在一起，以避免额外的网络请求</li>
<li>缺少文件会导致编译错误，而不是给用户 404 错误</li>
<li>构建产物的文件名包含内容哈希，因此你无需担心浏览器会缓存旧版本</li>
</ul>
<h4 id="何时使用public"><a href="#何时使用public" class="headerlink" title="何时使用public"></a>何时使用public</h4><p>通常我们建议从 JavaScript 导入 stylesheets，图片和字体。 <code>public</code> 文件夹可用作许多不常见情况的变通方法：</p>
<ul>
<li>你需要在构建输出中具有特定名称的文件，例如 <code>manifest.json</code></li>
<li>你有数千张图片，需要动态引用它们的路径</li>
<li>你希望在打包代码之外包含一个无需走构建逻辑的小脚本</li>
<li>在工程中，某些库可能与 webpack 不兼容，你没有其他选择，只能将其放在 <code>public</code> 中引入</li>
</ul>
<h3 id="代码质量保障"><a href="#代码质量保障" class="headerlink" title="代码质量保障"></a>代码质量保障</h3><p>为了保证代码质量，我们推荐使用 lint 相关的工具对代码进行检测，同时为了降低常规 lint 工具的使用成本，我们封装了 <a href="https://github.com/ice-lab/spec" target="_blank" rel="noopener">@iceworks/spec</a> 这个 npm 包，基础的 eslint 规则与<a href="https://f2e.alibaba-inc.com/specification/" target="_blank" rel="noopener">阿里巴巴前端规范</a>保持一致。</p>
<h4 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//eslint使用方法如下</span></span><br><span class="line"><span class="comment">// .eslintrc.js。JavaScript工程</span></span><br><span class="line"><span class="keyword">const</span> &#123; getESLintConfig &#125; = <span class="built_in">require</span>(<span class="string">'@iceworks/spec'</span>);</span><br><span class="line"><span class="built_in">module</span>.exports = getESLintConfig(<span class="string">'rax'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// .eslintrc.js。TypeScript工程</span></span><br><span class="line"><span class="keyword">const</span> &#123; getESLintConfig &#125; = <span class="built_in">require</span>(<span class="string">'@iceworks/spec'</span>);</span><br><span class="line"><span class="built_in">module</span>.exports = getESLintConfig(<span class="string">'rax-ts'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">//stylelint 用来检测样式代码的风格，新建配置文件 .stylelintrc.js 引入 lint 规则：</span></span><br><span class="line"><span class="comment">// .stylelintrc.js</span></span><br><span class="line"><span class="keyword">const</span> &#123; getStylelintConfig &#125; = <span class="built_in">require</span>(<span class="string">'@iceworks/spec'</span>);</span><br><span class="line"><span class="built_in">module</span>.exports = getStylelintConfig(<span class="string">'rax'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">//用于规范 commit message 的规范，防止全是 fix 这种无意义的 commit message 导致历史记录追溯比较麻烦，新建配置文件 .commitlintrc.js 引入规则：</span></span><br><span class="line"><span class="comment">// .commitlintrc.js</span></span><br><span class="line"><span class="keyword">const</span> &#123; getCommitlintConfig &#125; = <span class="built_in">require</span>(<span class="string">'@iceworks/spec'</span>);</span><br><span class="line"><span class="built_in">module</span>.exports = getCommitlintConfig(<span class="string">'rax'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// package.json</span></span><br><span class="line"><span class="string">"scripts"</span>: &#123;</span><br><span class="line">  <span class="string">"lint"</span>: <span class="string">"npm run eslint &amp;&amp; npm run stylelint"</span>,</span><br><span class="line">  <span class="string">"eslint"</span>: <span class="string">"eslint --cache --ext .js,.jsx,.ts,.tsx ./"</span>,</span><br><span class="line">  <span class="string">"stylelint"</span>: <span class="string">"stylelint **/*.&#123;css,scss,less&#125;"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>通过 npm scripts 配置命令：这样就可以通过npm run lint来运行lint任务了。</p>
<p>如果遇到 js 项目 ESLint 问题可以实时反馈而 ts 项目无效时，可以打开 VS Code setting ，在搜索框中输入 ‘ eslint validate ’ ，勾选相关选项。</p>
<h4 id="迁移"><a href="#迁移" class="headerlink" title="迁移"></a>迁移</h4><p>从自定义配置迁移</p>
<ol>
<li>移除项目中的各类 ESLint plugin config 及 parser。</li>
<li>安装 @iceworks/spec。</li>
<li>参考上述文档修改 lint 配置。</li>
</ol>
<p>**注：一定要清除项目之前的 ESLint 的 **<strong>plugin config 及 parser 相关包。</strong></p>
<h2 id="代码分割"><a href="#代码分割" class="headerlink" title="代码分割"></a>代码分割</h2><p>代码切割（Code Splitting） 能够把代码切割到不同的 bundle 中，实现按需或并行加载。代码切割可以用于获取更小的 bundle，以及控制资源加载优先级，如果使用合理，会极大减少首屏资源加载时间。</p>
<h3 id="dynamic-import"><a href="#dynamic-import" class="headerlink" title="dynamic import"></a>dynamic import</h3><p>使用 <code>import()</code>，webpack 会在编译阶段对引入的资源进行代码切割，即只有当运行时逻辑执行到 <code>import()</code> 调用点时才会加载对应的资源，该函数返回值是 <code>Promise</code>，开发者可以在链式回调中获取到引用的模块资源。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; isWeb &#125; <span class="keyword">from</span> <span class="string">'@uni/env'</span>;</span><br><span class="line"><span class="keyword">if</span> (isWeb) &#123;</span><br><span class="line">  <span class="keyword">import</span>(<span class="string">'./fetch'</span>).then(<span class="function"><span class="params">fetch</span> =&gt;</span> &#123;</span><br><span class="line">    fetch(<span class="string">'m.taobao.com'</span>);</span><br><span class="line">  &#125;).catch(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.error(<span class="string">'模块引入失败！'</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="rax-use-import"><a href="#rax-use-import" class="headerlink" title="rax-use-import"></a>rax-use-import</h3><p>函数式组件本身不能是一个异步函数，所以在使用 <code>import()</code> 动态加载子组件时会存在异步更新视图的处理，为了简化对应场景，我们提供了 <code>rax-use-import</code> 可以在函数组件中用同步的写法快速使用动态加载子组件的能力。</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createElement &#125; <span class="keyword">from</span> <span class="string">'rax'</span>;</span><br><span class="line"><span class="keyword">import</span> useImport <span class="keyword">from</span> <span class="string">'rax-use-import'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> [Bar, error] = useImport(<span class="function"><span class="params">()</span> =&gt;</span> <span class="keyword">import</span>(<span class="comment">/* webpackChunkName: "bar" */</span> <span class="string">'./Bar'</span>));</span><br><span class="line">  <span class="keyword">if</span> (error) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">p</span>&gt;</span>error<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (Bar) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">Bar</span> /&gt;</span></span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">p</span>&gt;</span>loading<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span>;  </span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="插件开发"><a href="#插件开发" class="headerlink" title="插件开发"></a>插件开发</h2><p>rax-app 基于工程构建工具 build-scripts，提供了丰富的插件用于提升项目的开发体验。</p>
<p>rax-app 基于工程构建工具 build-scripts 封装，因此在插件能力上也完整继承了 build-scripts。除了通过插件定制工程能力以外，rax-app 还为插件扩展了运行时定制的能力，这让插件拥有更多的想象空间。</p>
<p>插件机制是 rax-app 的核心之一，当前 rax-app 的基础能力都是通过插件来实现。插件机制不但可以保证框架核心足够精简和稳定，还可以通过插件对运行时和编译时的能力进行封装复用，最终打造一个完整的生态。</p>
<h3 id="plugin-rax-compat-react"><a href="#plugin-rax-compat-react" class="headerlink" title="plugin-rax-compat-react"></a>plugin-rax-compat-react</h3><p>用于在 Rax 项目里兼容使用 React 组件。在 Rax App 项目里，当没有设置 <code>store: false</code> 时，默认就已经做了相关兼容处理，无需再添加该插件。</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"plugins"</span>: [</span><br><span class="line">    <span class="string">"build-plugin-rax-compat-react"</span></span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="初始化插件"><a href="#初始化插件" class="headerlink" title="初始化插件"></a>初始化插件</h3><p>在项目根目录下新建 <code>build.plugin.js</code> 文件，然后写入以下代码：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// build.plugin.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123; context, onGetWebpackConfig &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  onGetWebpackConfig(<span class="function">(<span class="params">config</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'Get Webpack Config'</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//然后再build.json里引入自定义插件即可，在plugin数组中添加上即可。</span></span><br><span class="line"><span class="comment">//如果自定义逻辑比较复杂且需要跨项目使用，推荐将插件发布为一个 npm 包</span></span><br></pre></td></tr></table></figure>

<p>插件入口是一个fucntion，代码如下：</p>
<p>在执行 start 或者 build 命令时会按照顺序调用每个插件的入口方法。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123; context, onGetWebpackConfig, ...restApi &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  onGetWebpackConfig(<span class="function">(<span class="params">config</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'Get Webpack Config'</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="插件API"><a href="#插件API" class="headerlink" title="插件API"></a>插件API</h3><p>插件入口方法接受的参数即插件 API，API 列表如下：</p>
<p>1、context：包含运行时的各种环境信息。</p>
<p>command：当前运行命令，如start、build、test；commandArgs<code> script 命令执行时接受到的参数；</code>rootDir<code> 项目根目录；</code>userConfig<code> 用户在 build.json 中配置的内容；</code>pkg`项目 package.json 中的内容</p>
<p>2、onGetWebpackConfig：通过 <code>onGetWebpackConfig</code> 获取 webpack-chain 形式的配置，并对配置进行自定义修改：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;onGetWebpackConfig, registerTask&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  registerTask(<span class="string">'default'</span>, webpackConfig);</span><br><span class="line"></span><br><span class="line">  onGetWebpackConfig(<span class="function">(<span class="params">config</span>) =&gt;</span> &#123;</span><br><span class="line">    config.entry(<span class="string">'xxx'</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>3、onGetJestConfig：通过 <code>onGetJestConfig</code> 获取 jest 配置，可对配置进行自定义修改：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;onGetJestConfig&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  onGetJestConfig(<span class="function">(<span class="params">jestConfig</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> modifiedJestConfig = modify(jestConfig);</span><br><span class="line">      <span class="keyword">return</span> modifiedJestConfig;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>4、onHook：通过 onHook 监听命令运行时事件，onHook 注册的函数执行完成后才会执行后续操作，可以用于在命令运行中途插入插件想做的操作：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123; onHook &#125;</span>) =&gt;</span> &#123;</span><br><span class="line"> onHook(<span class="string">'before.build.load'</span>, () =&gt; &#123;</span><br><span class="line">   <span class="comment">// do something before dev</span></span><br><span class="line"> &#125;);</span><br><span class="line"> onHook(<span class="string">'after.build.compile'</span>, (stats) =&gt; &#123;</span><br><span class="line">   <span class="comment">// do something after build</span></span><br><span class="line"> &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>目前支持的命令执行生命周期如下：</p>
<p><img src="/../../image/Rax%E5%88%9D%E5%AD%A6/start.png" alt="start"></p>
<p><img src="/../../image/Rax%E5%88%9D%E5%AD%A6/test.png" alt="test"><img src="/../../image/Rax%E5%88%9D%E5%AD%A6/build.png" alt="build"></p>
<p>5、log：统一的 log 工具，底层使用 <code>npmlog</code> ，便于生成统一格式的 log。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">log.info(<span class="string">'start'</span>);</span><br><span class="line">log.verbose(<span class="string">'debug'</span>);</span><br><span class="line">log.error(<span class="string">'exit'</span>);</span><br></pre></td></tr></table></figure>

<p>6、registerUserConfig：为用户配置文件 <code>build.json</code> 中添加自定义字段。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;registerUserConfig&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  registerUserConfig(&#123;</span><br><span class="line">    name: <span class="string">'custom-key'</span>,</span><br><span class="line">    validation: <span class="string">'boolean'</span> <span class="comment">// 可选，支持类型有 string, number, array, object, boolean</span></span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>7、registerCliOption：为命令行启动添加自定义参数。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;registerCliOption&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  registerCliOption(&#123;</span><br><span class="line">    name: <span class="string">'custom-option'</span>, <span class="comment">// 参数名</span></span><br><span class="line">    commands: [<span class="string">'start'</span>], <span class="comment">// 命令</span></span><br><span class="line">    configWebpack: <span class="function">(<span class="params">arg</span>) =&gt;</span> &#123;&#125; <span class="comment">// 可选，arg 为命令行参数对应值</span></span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>8、registerMethod：注册自定义方法。通过 <code>applyMethod</code> 调用。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;registerMethod&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  registerMethod(name, func); <span class="comment">// name, func 分别为方法名和方法</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>9、modifyUserConfig：修改用户配置文件。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;modifyUserConfig&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  modifyUserConfig(key, value); <span class="comment">// key, value 分别为用户配置文件键值对</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>10、registerTask：添加 webpack 配置，配置为 webpack-chain 形式。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;registerTask&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  registerTask(name, config); <span class="comment">// name: Task名, config: webpack-chain 形式的配置</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>11、getAllTask：获取所有 webpack 配置名称。getAllPlugin：获取所有插件。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;getAllTask&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> alltasks = getAlltask();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;getAllPlugin&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 获取所有插件数组</span></span><br><span class="line">  <span class="comment">// 类型：() =&gt; [&#123;pluginPath, options, name&#125;]</span></span><br><span class="line">  <span class="keyword">const</span> plugins = getAllPlugin(); ，[]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="扩展API"><a href="#扩展API" class="headerlink" title="扩展API"></a>扩展API</h3><p>除了以上由 build-scripts 内置支持的 API，我们还通过 rax-app 对插件 API 做了扩展，扩展的 API 需要通过以下方式调用：</p>
<p>目前扩展的 API 仅支持同步调用。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123; applyMethod &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 第一个参数对应 API 名称，第二个参数对应 API 参数</span></span><br><span class="line">  applyMethod(<span class="string">'addExport'</span>, &#123; <span class="attr">source</span>: <span class="string">`./config`</span>, exportName &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>addExport</p>
<p>向 <code>rax-app</code> 里注册模块，实现 <code>import { foo } from &#39;rax-app&#39;;</code> 的能力：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 实现 import &#123; request &#125; from 'rax-app'; request 由插件的 `./request/request` 文件实现</span></span><br><span class="line">applyMethod(<span class="string">'addExport'</span>, &#123; <span class="attr">source</span>: <span class="string">'./request/request'</span>, <span class="attr">exportName</span>: <span class="string">'request'</span> &#125;)</span><br></pre></td></tr></table></figure>

<p>removeExport</p>
<p>与 <code>addExport</code> 对应：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.applyMethod(<span class="string">'removeExport'</span>, <span class="string">'store'</span>);</span><br></pre></td></tr></table></figure>

<p>addAppConfigTypes</p>
<p>向 appConfig 添加类型</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 第一项参数对应 API 名称，第二项参数对应 API 参数。</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// API 参数：</span></span><br><span class="line"><span class="comment">// source: 类型声明文件。./foo/types，对应 TEMP_DIR/foo/types。 TEMP_DIR，可通过 getValue('TEMP_PATH') 获得。注意：需先将对应类型文件移至 TEMP_DIR。</span></span><br><span class="line"><span class="comment">// specifier: 导出类型标识符，可选，默认值为 '*'。</span></span><br><span class="line"><span class="comment">// exportName: 添加至 appConfig 类型 IAppConfig 上的导出名。</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// 结果为：</span></span><br><span class="line"><span class="comment">// // TEMP_DIR/types.ts</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// import &#123; Foo &#125; from './foo/types';</span></span><br><span class="line"><span class="comment">// export interface IAppConfig &#123;</span></span><br><span class="line"><span class="comment">//   foo?: Foo</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br><span class="line">applyMethod(<span class="string">'addAppConfigTypes'</span>, &#123; <span class="attr">source</span>: <span class="string">`./foo/types`</span>, <span class="attr">specifier</span>: <span class="string">'&#123; Foo &#125;'</span>, <span class="attr">exportName</span>: <span class="string">`foo?: Foo`</span> &#125;);</span><br></pre></td></tr></table></figure>

<p>removeAppConfigTypes</p>
<p>与 <code>addAppConfigTypes</code> 对应</p>
<p>getPages</p>
<p>获取 <code>src/pages</code> 下的一级页面列表：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ['Home', 'About']</span></span><br><span class="line"><span class="keyword">const</span> pages = <span class="keyword">this</span>.applyMethod(<span class="string">'getPages'</span>, <span class="keyword">this</span>.rootDir);</span><br></pre></td></tr></table></figure>

<p>watchFileChange</p>
<p>监听 <code>src</code> 下的文件变化：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">applyMethod(<span class="string">'watchFileChange'</span>, <span class="string">'src/config.*'</span>, <span class="keyword">async</span> (event: string) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (event === <span class="string">'unlink'</span> || event === <span class="string">'add'</span>) &#123;</span><br><span class="line">    <span class="comment">// do something</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<h2 id="插件参数"><a href="#插件参数" class="headerlink" title="插件参数"></a>插件参数</h2><p>用户可以在 <code>build.json</code> 中指定插件参数：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"plugins"</span>: [</span><br><span class="line">    [<span class="string">"build-plugin-rax-example"</span>, &#123;</span><br><span class="line">      <span class="attr">"type"</span>: <span class="string">"bar"</span></span><br><span class="line">    &#125;]</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>插件入口函数的第二个参数即用户自定义配置：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123; context, log &#125;, options</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; type &#125; = options;</span><br><span class="line">  log.info(type); <span class="comment">// =&gt; bar</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="插件通信"><a href="#插件通信" class="headerlink" title="插件通信"></a>插件通信</h2><p>插件间需要进行通信的场景诉求：</p>
<ol>
<li>不同插件之间需要知道彼此的存在来确定是否执行相应的逻辑</li>
<li>多个插件共有的配置信息可以抽出来，在某个插件中进行配置</li>
</ol>
<p>使用 <code>setValue</code> 和 <code>getValue</code> 两个API来实现，分别用于数据的存取。</p>
<h3 id="setValue"><a href="#setValue" class="headerlink" title="setValue"></a>setValue</h3><p>类型：<code>(key: string | number, value: any) =&gt; void</code>，示例：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// build-plugin-test</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123; setValue &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  setValue(<span class="string">'key'</span>, <span class="number">123</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="getValue"><a href="#getValue" class="headerlink" title="getValue"></a>getValue</h3><p>类型：<code>(key: string | number) =&gt; any</code>，示例：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">&#123;getValue&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> value = getValue(<span class="string">'key'</span>); <span class="comment">// 123</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>同时在 rax-app 中也内置了几个变量：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> projectType = getValue(<span class="string">'PROJECT_TYPE'</span>); <span class="comment">// ts|js</span></span><br><span class="line"><span class="keyword">const</span> dirPath = getValue(<span class="string">'TEMP_PATH'</span>);  <span class="comment">// 对应 .rax 的路径</span></span><br></pre></td></tr></table></figure>

<h2 id="类型"><a href="#类型" class="headerlink" title="类型"></a>类型</h2><p>接口类型通过以下方法引入：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; IPlugin &#125; <span class="keyword">from</span> <span class="string">'@alib/build-scripts'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> plugin: IPlugin = <span class="keyword">async</span> (&#123; getValue, applyMethod, onGetWebpackConfig &#125;): <span class="built_in">Promise</span>&lt;<span class="keyword">void</span>&gt; =&gt; &#123;</span><br><span class="line">  onGetWebpackConfig(<span class="function">(<span class="params">config</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> plugin;</span><br></pre></td></tr></table></figure>

      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/16/react/react%E4%BF%AE%E7%82%BC(%E5%85%AD)-Webpack%E9%85%8D%E7%BD%AE/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/16/react/react%E4%BF%AE%E7%82%BC(%E5%85%AD)-Webpack%E9%85%8D%E7%BD%AE/" class="post-title-link" itemprop="url">react修炼（六）-Webpack配置</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
    
              <time title="创建时间：2021-11-16 12:18:39" itemprop="dateCreated datePublished" datetime="2021-11-16T12:18:39+08:00">2021-11-16</time>
            </span>
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="far fa-calendar-check"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2021-11-13 22:43:34" itemprop="dateModified" datetime="2021-11-13T22:43:34+08:00">2021-11-13</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/react/" itemprop="url" rel="index"><span itemprop="name">react</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>webpack本质上是一个打包工具，它会根据代码的内容解析模块依赖，帮助用户把多个模块的代码打包。</p>
<h1 id="Webpack入门"><a href="#Webpack入门" class="headerlink" title="Webpack入门"></a>Webpack入门</h1><h2 id="基础配置"><a href="#基础配置" class="headerlink" title="基础配置"></a>基础配置</h2><p>webpack在4.x的版本是支持零配置的，不需要使用额外的配置文件webpack.config.js就可以执行简单的构建任务。webpack的默认配置如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">"path"</span>);</span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    entry: <span class="string">"./src/index.js"</span>,</span><br><span class="line">    output: &#123;</span><br><span class="line">        filename: <span class="string">"main.js"</span>,</span><br><span class="line">        path: path.resolve(__dirname, <span class="string">"./dist"</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>webpack默认出口entry是src/index.js；webpack的默认输出目录是./dist；webpack没有指定mode模式会出现警告，默认production生产默认。</p>
<p>配置文件:webpack.config.js。其中各个属性的基础概念如下：</p>
<p>entry入口：指定webpack执行构建任务的入口，一般为项目的入口文件；</p>
<p>output输出：告诉webpack在哪里输出它所创建的bundles，以及如何命名这些文件，默认值为./dist；</p>
<p>module模块：webpack是基于Node.js的，项目中的任何文件都可以看成module；</p>
<p>loader：模块转化器，用于对模块的源代码进行转换；</p>
<p>plugin插件：webpack的支柱功能，作用域webpack整个构建周期，plugin插件目的在于解决loader无法实现的任务；</p>
<p>bundle文件：最终打包生成的文件，比如默认配置下输出的”./dist/main.js”文件；</p>
<p>mode模式：通过配置其来制定是生产环境打包还是开发环境打包，比如：生产环境代码需要压缩，图片需要优化。</p>
<h2 id="核心配置"><a href="#核心配置" class="headerlink" title="核心配置"></a>核心配置</h2><h3 id="entry"><a href="#entry" class="headerlink" title="entry"></a>entry</h3><p>webpack的entry支持字符串、对象、数组类型，从作用上说，包括单文件入口和多文件入口两种方式。</p>
<p>单文件入口示例代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    entry: <span class="string">"./src/index.js"</span></span><br><span class="line">&#125;;</span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    entry: &#123;</span><br><span class="line">        main: <span class="string">"./src/index.js"</span>,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    entry: [<span class="string">"./src/index.js"</span>, <span class="string">"./src/other.js"</span>],</span><br><span class="line">    output : &#123;</span><br><span class="line">        filename: <span class="string">"bundle.js"</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>上面的配置，无论是字符串还是字符数组的entry，实际上都只有一个入口，但是在打包产出上会有差异：1、如果直接是string的形式，那么webpack就会直接把该string指定的模块作为入口模块；2、如果是数组[string]的形式，那么webpack会自动生成另外一个入口模块，并将数组每个元素指定的文件加载进来，并将最后一个模块的module.exports作为入口模块的module.exports导出。</p>
<p>多文件入口示例代码：是使用对象方式配置entry</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    entry : &#123;</span><br><span class="line">        index: <span class="string">"./src/index.js"</span>,</span><br><span class="line">        list: <span class="string">"./src/list.js"</span>,</span><br><span class="line">        detail: <span class="string">"./src/detail.js"</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">//上面的语法将entry分为了三个独立的入口文件，这样会打包出来3个对应的bundle文件</span></span><br></pre></td></tr></table></figure>

<h3 id="output"><a href="#output" class="headerlink" title="output"></a>output</h3><p>webpack的output是指定了entry对应文件编译打包后的输出bundle。output的常用属性如下：1、path：设置输出的bundle文件存放路径；2、filename：设置bundle文件的名称。当不设置output时，默认输出到dist.main.js。即output.pah是dist，output.filename是main.js。</p>
<p>一份webpack的配置可以多入口，但不能多出口。对于不同的entry可以通过output.filename占位符语法来区分。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    entry : &#123;</span><br><span class="line">        index: <span class="string">"./src/index.js"</span>,</span><br><span class="line">        list: <span class="string">"./src/list.js"</span>,</span><br><span class="line">        detail: <span class="string">"./src/detail.js"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    output: &#123;</span><br><span class="line">        path: path.resolve(__dirname, <span class="string">"./build"</span>);</span><br><span class="line">        filename: <span class="string">"[name]-[hash:8].js"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    mode: <span class="string">"development"</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>其中[name]，[hash:8]就是占位符，占位符是可以组合使用的；[name]：把入口配置的key作为输出模块的名称；[hash]：是整个项目的hash值，其根据每次编译内容计算得到，每次编译之后都会生成新的hash，即修改任何文件都会导致所有文件的hsah发生改变。</p>
<h3 id="module"><a href="#module" class="headerlink" title="module"></a>module</h3><p>webpack默认只支持解析Js和Json模块，对于别的类型模块，webpack默认不支持，这个时候就需要module配置了，不同的模块需要需要不同的loader来处理。module有以下两个配置：</p>
<p>module.noParse：配置项可以让webpack忽略对部分没采用模块化的文件的递归解析和处理，这样做的好处是能提高构建性能。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">"path"</span>);</span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    <span class="built_in">module</span>: &#123;</span><br><span class="line">        noParse: <span class="regexp">/jquery |lodash/</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>module.rules：是在处理模块时，将符合规则条件的模块，提交给对应的处理器来处理，通常用来配置loader，其类型是一个数组，数组里每一项都描述了如何去处理部分文件。我们常用条件匹配方式来匹配模块，条件匹配相关的配置常用的有test、include、exclude。示例代码如下：（匹配的条件为：来自src和test文件夹，不包含node_modules和bower_modules子目录，模块的文件路径为.tsx和.jsx结尾的文件）。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">"path"</span>);</span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    <span class="built_in">module</span>: &#123;</span><br><span class="line">        rules:[</span><br><span class="line">            &#123;</span><br><span class="line">                test: [<span class="regexp">/\.jsx?$/</span>, /\.tsx?$/],</span><br><span class="line">                include: [</span><br><span class="line">                    path.resolve(__dirname, <span class="string">'src'</span>),</span><br><span class="line">                    path.resolve(__dirname, <span class="string">'test'</span>)</span><br><span class="line">                ],</span><br><span class="line">                exclude: [</span><br><span class="line">                    path.resolve(__dirname, <span class="string">'node_modules'</span>),</span><br><span class="line">                    path.resolve(__dirname, <span class="string">'bower_modules'</span>)</span><br><span class="line">                ]</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h3 id="loader"><a href="#loader" class="headerlink" title="loader"></a>loader</h3><p>loader是模块解析器，用在module.rules字段中，配合条件匹配，对符合条件的模块进行转换。在使用对应的loader之前，需要先将其安装，例如:less-loader。先安装less-loader，再指定配置。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    <span class="built_in">module</span>: &#123;</span><br><span class="line">        rules: [</span><br><span class="line">            test: <span class="regexp">/\.less $/</span>,</span><br><span class="line">            use: <span class="string">'less-loader'</span></span><br><span class="line">        ]</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>test项使用/.less $/正则匹配需要处理的模块文件，然后都交给less-loader来处理，这里的less-loader是个string，最终会被作为require()的参数来直接使用。这样less文件都会被less-loader处理成对应的css文件。</p>
<p>有css-loader就可以识别CSS语法了；还需要style-loader将css-loader打包好的代码以<style>的形式插入到HTML文件中。</p>
<h3 id="plugin"><a href="#plugin" class="headerlink" title="plugin"></a>plugin</h3><p>plugin是webpack的重要组成部分，通过plugin来处理loader处理不了的内容。</p>
<h3 id="devtool"><a href="#devtool" class="headerlink" title="devtool"></a>devtool</h3><p>devtool来控制怎么显示sourcemap，通过sourcemap可以快速还原代码的错误位置。但由于sourcemap包含的数据量较大，而且生成算法需要一定的计算量支持，不同的值会明显影响到构建和重新构建的速度。 </p>
<h2 id="工程化配置"><a href="#工程化配置" class="headerlink" title="工程化配置"></a>工程化配置</h2><h3 id="静态资源管理"><a href="#静态资源管理" class="headerlink" title="静态资源管理"></a>静态资源管理</h3><p>前端项目离不开各种静态资源，静态资源指的是前端中常用的图片、富媒体、字体文件等。webpack中静态资源也是可以作为模块直接使用的。这里有两个loader需要借助使用：file-loader和url-loader。</p>
<p>file-loader：能够根据配置项复制使用到的资源(不局限于图片)到构建后的文件夹，并且能改变对应的链接。</p>
<p>url-loader：包含fiale-loader的全部功能，并且能够根据配置将符合配置的文件转换成Base64方式引入，将小体积的图片Base64引入项目可以减少http请求。</p>
<h3 id="本地开发环境配置"><a href="#本地开发环境配置" class="headerlink" title="本地开发环境配置"></a>本地开发环境配置</h3><p>经过上面的步骤，项目已经支持了样式和静态文件资源，但目前对修改代码后做调试时非常不方便，需要启动webpack，手动刷新浏览器。这里我们可以使用webpack-dev-server，这是一个基于Express的本地开发服务器，它使用webpack-dev-middleware中间件来为webpack打包生成的资源文件提供web服务。</p>
<p>执行webpack-dev-server命令后，它会读取webpack的配置文件，然后将文件打包到内存中，这时打开server的默认地址:localhost:8080就可以看到文件目录或页面。可以把webpack-dev-server放到package.json的scripts中。</p>
<p>1、自动刷新功能：用户希望边写代码，边看到页面的更新情况。webpack-dev-server提供的自动刷新功能有两种模式：iframe模式：页面被放到一个iframe中，当变化时会重新加载；incline模式：将webpack-dev-server的重载代码添加到产出的bundle中。</p>
<p>两种模式都支持模块热替换，其好处是只替换更新的部分，而不是整个页面重新加载。</p>
<p>2、模块热替换：HMR。</p>
<p>3、proxy：代理。在实际开发中，本地开发服务器是不会直接请求线上数据接口的，这是因为浏览器的同源安全策略导致的跨域问题，可以使用的vServer.proxy来解决本地开发跨域的问题。</p>
<h2 id="Babel"><a href="#Babel" class="headerlink" title="Babel"></a>Babel</h2><p>Babel可以把最新的ES语法的代码轻松转换成任意版本的JavaScript语法。Babel会在正在被转义的文件当前目录中查找一个.babelrc文件。如果不存在则会向外层目录遍历树，或有一个”babel”的package.json文件。</p>
<p> babel-loader是webpack与babel的通信桥梁，不会把es6语法转换成es5，这部分工作由@babel-preset-env来完成。</p>
<p>同样，我们还可以使用Babel-polyfill，按需引入垫片，对需要的项进行转译。</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//.babelrc</span></span><br><span class="line">&#123;</span><br><span class="line">    presets : [</span><br><span class="line">        "@babel/preset-env",</span><br><span class="line">        &#123;</span><br><span class="line">            target: &#123;</span><br><span class="line">                edge: "17",</span><br><span class="line">                firefox: "60",</span><br><span class="line">                chrome: "67",</span><br><span class="line">                safari: "11.1"</span><br><span class="line">            &#125;,</span><br><span class="line">            core.js: 2,//新版本需要指定核心库版本</span><br><span class="line">            useBuiltIns: "usage" //按需注入polyfill</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">"@babel/preset-react"</span><span class="comment">//在webpack中安装babel与react转换的插件，继承React开发环境。</span></span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//webpack.config.js</span></span><br><span class="line">&#123;</span><br><span class="line">    test: /\.js $/,</span><br><span class="line">    exclude: /node_modules/,</span><br><span class="line">    loader: "babel-loader"</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="扩展优化"><a href="#扩展优化" class="headerlink" title="扩展优化"></a>扩展优化</h2><h3 id="配置优化"><a href="#配置优化" class="headerlink" title="配置优化"></a>配置优化</h3><p>1、缩小文件范围：loader是个值得优化的部分，应该尽可能少地使用loader，可以通过exclude、include配置来确保转译尽可能少的文件。exclude指定要排除的文件,include指定要包含的文件。</p>
<p>exclude的优先级高于include，在include和exclude中使用绝对路径数组，尽量避免exclude更倾向于使用include。</p>
<p>2、优化Resolve.modules配置，用于配置webpack去哪些目录下寻找第三方模块，默认是[‘node_modules’]；如果没有找到，就会递归的方式往父级目录的node_modules文件夹下面找；如果第三方模块都安装在了项目根目录下，就可以直接指明这个路径，不用再去一个个寻找；</p>
<p>3、优化Resolve.alias配置：通过别名来将原导入路径映射成一个新的导入路径2，以React为例，引入的React库一般存在两套代码：cjs格式，采用commonJS规范的模块化代码；umd格式，已经打包好的完整代码，没有采用模块化，可以直接执行。</p>
<p>默认情况下，webpack会从入口文件./node_modules/bin/react/index开始递归解析和处理依赖的文件。可以直接指定文件，避免此处的耗时。</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">alias: &#123;</span><br><span class="line">    "@": path.join(__dirname, "./pages"),</span><br><span class="line">    react: path.resolve(__dirname, "./node_modules/react/umd/react.production.min.js"),</span><br><span class="line">    "react-dom": path.resolve(__dirname, "./node_modules/react-dom/umd/react-dom.production.min.js")</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>4、优化Resolve.extension配置。</p>
<p>Resolve.extension在导入语句没带文件格式后置时，webpack会去Resolve.extension数组泪飙中查找，会消耗事时间，因此使用模块导入时需要加上文件格式后缀。</p>
<h3 id="资源文件优化"><a href="#资源文件优化" class="headerlink" title="资源文件优化"></a>资源文件优化</h3><p>1、开启CDN引入，动静分离，将静态资源部署在CDN服务器上。</p>
<p>2、样式文件处理：如果不做抽取配置，css文件是直接打包进js文件里的，我们希望能单独生成css文件，这样的话,css和js可以并行下载，提高页面的加载速度。</p>
<p>我们这里借助MiniCssExtractPlugin完成抽离css；借助potimize-css-assets-webpack-plugin，cssnano，减少体积。</p>
<p>3、html文件处理：借助html-webpack-plugin插件可以完成对html文件体积的压缩；</p>
<h3 id="代码优化"><a href="#代码优化" class="headerlink" title="代码优化"></a>代码优化</h3><p>1、tree-shaking：JS中tree-shaking只支持ES module的引入方式。Dead Code代码的特点：1、代码不会被执行、不可到达；2、代码执行的结果不会被用到；3、代码只会影响死的变量(只读不写)。</p>
<p>开启摇树功能后，虽然可以压缩代码体积，但是会产生一定的副作用，比如在index.js入口模块中引入图片模块、样式模块，但这些模块没有在语句中用到，但这些模块没有在语句中被用到，因此会被摇掉，此时需要修改package.json模块，避免副作用产生。或者在数组里排除不需要tree-shaking的模块。</p>
<p>2、代码分割：设置splitChunks.chunks = all；</p>
<p>3、使用HardSourceWebpackPlugin缩短构建时间。</p>

      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/14/react/react%E4%BF%AE%E7%82%BC(%E4%BA%94)-React%E6%BA%90%E7%A0%81/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/14/react/react%E4%BF%AE%E7%82%BC(%E4%BA%94)-React%E6%BA%90%E7%A0%81/" class="post-title-link" itemprop="url">react修炼（五）-React源码</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
    
              <time title="创建时间：2021-11-14 12:18:39" itemprop="dateCreated datePublished" datetime="2021-11-14T12:18:39+08:00">2021-11-14</time>
            </span>
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="far fa-calendar-check"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2021-11-12 16:42:51" itemprop="dateModified" datetime="2021-11-12T16:42:51+08:00">2021-11-12</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/react/" itemprop="url" rel="index"><span itemprop="name">react</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>React源码的基础内容解析。</p>
<h2 id="常用变量"><a href="#常用变量" class="headerlink" title="常用变量"></a>常用变量</h2><p>React源码中有很多常用变量，如标记节点类型的fiber.tag等，提前了解这些对阅读源码很有帮助。</p>
<h3 id="WorkTag"><a href="#WorkTag" class="headerlink" title="WorkTag"></a>WorkTag</h3><p>WorkTag用于标记React中的不同元素，如原生HTML标签元素、函数组件、class组件、Provider组件、Consumer组件，这些通常都体现在fiber的tag值上。</p>
<h3 id="SideEffectTag"><a href="#SideEffectTag" class="headerlink" title="SideEffectTag"></a>SideEffectTag</h3><p>SideEffectTag用来标记React中更新的类型，如没有更新是NoEffect，插入为Placement。</p>
<p>这里的effectTag都是二进制，这个和React中用到的位运算有关。位运算只能用于整数，并且是直接对二进制位进行计算，直接处理每一个比特位，是非常底层的运算，因此运算速度快。例如：workInProgress.effectTag为132，此时，workInProgress.effectTag &amp; Update和workInProgress.effectTag &amp; Ref在布尔值上都是true，这个时候既要执行update effect，还要执行ref update。 再例如workInProgress.effectTag | = Placement；这里就是指给workInProgress添加一个Placement的副作用。</p>
<h3 id="ExecutionContext"><a href="#ExecutionContext" class="headerlink" title="ExecutionContext"></a>ExecutionContext</h3><p>ExecutionContext用来标记React执行栈中目前所处的环境，所对应的全局变量是react-reconciler/src/ReactFiberWorkLoop.js文件中的executionContext。同样用到了位运算</p>
<h3 id="PriorityLevel"><a href="#PriorityLevel" class="headerlink" title="PriorityLevel"></a>PriorityLevel</h3><p>PriorityLevel用来标记更新的优先级，如提交更新时用ImmediatePriority，即立即执行的优先级，而用户交互事件的优先级时UserBlockingPriority。</p>
<h3 id="RootTag"><a href="#RootTag" class="headerlink" title="RootTag"></a>RootTag</h3><p>RootTag用来标记模式类型，目前默认是LegacyRoot模式，BlockingRoot和ConcurrentRoot模式均处于实验阶段。</p>
<h3 id="RootExitStatus"><a href="#RootExitStatus" class="headerlink" title="RootExitStatus"></a>RootExitStatus</h3><p>RootExitStatus这里标记了根节点退出时的状态值，未完成为RootIncomplete，已完成为RootCompleted。</p>
<h3 id="currentEventTime"><a href="#currentEventTime" class="headerlink" title="currentEventTime"></a>currentEventTime</h3><p>过期事件是根据加上当前时间的出来的。在React中，如果两个Update是在一个事件上进行调度的，就把它们的开始事件当做同一个。换句话说，由于是过期事件决定了Update是如何批量执行的，我们希望相似优先级并且发生在同一个事件上的update接收相同的过期时间。</p>
<h1 id="React源码"><a href="#React源码" class="headerlink" title="React源码"></a>React源码</h1><h2 id="初次渲染与更新"><a href="#初次渲染与更新" class="headerlink" title="初次渲染与更新"></a>初次渲染与更新</h2><p>ReactDOM.render可以触发初次渲染与更新，setState、forceUpdate和Hook方法等均可以触发更新，后面会一一介绍。</p>
<h3 id="ReactDOM-render"><a href="#ReactDOM-render" class="headerlink" title="ReactDOM.render"></a>ReactDOM.render</h3><p>ReactDOM.render通常是项目的入口文件。ReactDOM.render(element, container [, callback]);</p>
<p>其中element是要渲染的React元素，container则是容器，在初次渲染或更新完成后，如果定义了callback回调，则会执行callback。以下是render函数的定义，可以看到它实际上调用了legacyRenderSubtreeIntoContainer:</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params">	element: React $ Element&lt;any&gt;,</span></span></span><br><span class="line"><span class="function"><span class="params">    container: Container,</span></span></span><br><span class="line"><span class="function"><span class="params">    callback: ?Function,</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> legacyRenderSubtreeIntoContainer(<span class="literal">null</span>, element, container, <span class="literal">false</span>, callback);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>legacyRenderSubtreeIntoContainer里会判断是否是初次渲染，若是初次渲染则创建FiberRoot节点，下一步非批量执行updateContainer；如果不是初次渲染，则直接执行updateContainer。这两次都会判断callback回调是否存在，存在的话，则传参给updateContainer。</p>

      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/12/react/react%E4%BF%AE%E7%82%BC(%E4%B8%89)-router/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/12/react/react%E4%BF%AE%E7%82%BC(%E4%B8%89)-router/" class="post-title-link" itemprop="url">react修炼（三）-router</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              
    
              <time title="创建时间：2021-11-12 12:18:39 / 修改时间：15:21:35" itemprop="dateCreated datePublished" datetime="2021-11-12T12:18:39+08:00">2021-11-12</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/react/" itemprop="url" rel="index"><span itemprop="name">react</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在React-Router中，WEB端的Router提供了两种不同的模式：hash模式和history模式。hash模式即HashRouter是基于hash实现的一种路由方式，URL变化时主要是hash值进行变化。hash模式的好处是一定不会向服务端发送请求，但URL里一定会有一个#号。</p>
<p>BrowserRouter则是基于H5 historyAPI的一种路由方式。history的URL切换基于history提供的pushState方法。在项目上，使用时需要在项目的最外层配置好路由，并说明使用的是哪种路由。</p>
<p>上面的两种模式只是提供了不同的路由选择，本质上真正的路由还是使用Route组件，当浏览器中的URL与Router组件中的path匹配时，会呈现对应的UI。</p>
<p>path匹配默认情况下是一种模糊匹配，若当前URL是以该path开始的就会被匹配。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;Route path=<span class="string">"/"</span> component=&#123;IndexView&#125; /&gt;</span><br><span class="line">&lt;Route path=<span class="string">"/about"</span> component=&#123;AboutView&#125; /&gt;</span><br><span class="line">&lt;Route path=<span class="string">"/about/details"</span> component=&#123;AboutDetailsView&#125; /&gt;</span><br></pre></td></tr></table></figure>

<p>当浏览器URL为/about/details时，会发现这三个路由都会被渲染。如果希望URL和path必须一致时才匹配，就可以使用精确匹配，只要把Route的exact属性设置为true。</p>
<p>1、多路径匹配：path中传递一个数组；数组中存放多个路径，当其中一个路径与URL匹配时，则认定该路由匹配当前URL。</p>
<p>2、path参数，及通过path进行一些参数的传递。例如：路由信息，或者路由参数params的传递。</p>
<p>路由渲染：通过路由渲染视图的常用写法有以下两种：component和render。</p>
<p>1、component：使用Route的component属性渲染视图时，，要给component传入一个组件，该组件是路由匹配时要渲染的视图。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React, &#123;Fragment&#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;Route&#125; <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">About</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>关于我们视图内容<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">Fragment</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">Route</span> <span class="attr">path</span>=<span class="string">"/about"</span> <span class="attr">component</span>=<span class="string">&#123;About&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="xml">            <span class="tag">&lt;/<span class="name">Fragment</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>2、render，使用Route的render属性渲染视图时，要给render属性传递一个函数，函数必须有返回值，可以是视图组件或者JSX。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React, &#123;Fragment&#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;Route&#125; <span class="keyword">from</span> <span class="string">"react-router-dom"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">About</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>关于我们视图内容<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">Fragment</span>&gt;</span></span></span><br><span class="line">        &lt;Route path="/about" render=&#123;() =&gt; (&lt;div&gt;用户中心视图&lt;div&gt;)&#125; /&gt;</span><br><span class="line">            &lt;/Fragment&gt;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h1 id="路由信息"><a href="#路由信息" class="headerlink" title="路由信息"></a>路由信息</h1><p>被路由直接调用的组件一般称之为路由组件，Route在调用路由组件时会给该组件传递history、location、match三个参数，从这三个参数中可以获取到当前路由的一些信息。</p>
<p>1、history是在H5 history基础上封装处理的一个新对象，该对象下常用方法和属性如下。l</p>
<p>ength：该域下历史记录的条目；location：URL的信息，具体参考location对象；</p>
<p>push(path, [state])：将新条目推入历史记录对战，这里也是跳转URL的方法。例如单击按钮时想要跳转链接，就可以使用history.push(“url”)。state要传递给新的路由组件的额外信息，会被新路由组件中loaction的state接收；</p>
<p>replace(path,[state]):替换历史记录上的当前条目。这样同样会引发URL跳转，不同的是：此时会在历史记录中替换掉之前的记录。</p>
<p>go(n)，跳转n步历史记录，正数向前n步，负数后退n步。</p>
<p>2、location对象：浏览器地址栏相关信息，常用属性如下：</p>
<p>pathname：URL的路径，格式为字符串；search：URL中的search片段，格式为字符串；hash：URL中的hash片段，格式为字符串；state：路由跳转时传递的额外信息，如push(path, state)推送过来的state信息，格式为对象。</p>
<p>3、match对象：路由的匹配信息，常用属性如下：</p>
<p>params：接收path参数(:page)传递过来的相关信息，格式为对象；isExact：指是否精确匹配；path：Route中定义的path路径。</p>
<p>在获取路由信息时，只有通过Route component调用的组件才会被传递路由信息。而render调用时Route会把路由信息传递给函数，若想把参数传递给组件则需要开发者手动传递。</p>
<h2 id="非路由组件"><a href="#非路由组件" class="headerlink" title="非路由组件"></a>非路由组件</h2><p>非路由组件有时候也要获取路由信息，在React Router方法中提供了两种方法来方便开发者获取。</p>
<h3 id="withRouter"><a href="#withRouter" class="headerlink" title="withRouter"></a>withRouter</h3><p>withRouter的作用有点类似于Redux中的connnect，把要获取路由信息的组件传入withRouter，withRouter会把路由信息传递给该组件，并会返回一个新的组件，来方便其他地方调用。具体使用示例如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;withRouter&#125; <span class="keyword">from</span> <span class="string">'react-router-dom'</span>;</span><br><span class="line">fucntion backBtn(props) &#123;</span><br><span class="line">    <span class="keyword">let</span> &#123;history&#125; = props;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span> = <span class="string">&#123;()</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="xml">       history.goBack(); </span></span><br><span class="line"><span class="xml">    &#125;&#125;&gt;返回上一页<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line">backBtn = withRouter(backBtn);</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> backBtn;</span><br></pre></td></tr></table></figure>

<h3 id="Router-Hooks"><a href="#Router-Hooks" class="headerlink" title="Router Hooks"></a>Router Hooks</h3><p>使用规则与React中的其他Hooks一样；useHistory调用该Hook会返回history对象，useLocation调用该Hook会返回location对象，useRouteMatch调用该Hook会返回match对象，useParams调用该Hook会返回match对象中的params，也就是path传递的参数。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;useHistory&#125; <span class="keyword">from</span> <span class="string">'react-router-dom'</span>;</span><br><span class="line">fucntion backBtn(props) &#123;</span><br><span class="line">    <span class="keyword">let</span> history = useHistory;</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span> = <span class="string">&#123;()</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="xml">       history.goBack(); </span></span><br><span class="line"><span class="xml">    &#125;&#125;&gt;返回上一页<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line">backBtn = withRouter(backBtn);</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> backBtn;</span><br></pre></td></tr></table></figure>

<h2 id="链接跳转"><a href="#链接跳转" class="headerlink" title="链接跳转"></a>链接跳转</h2><p>如果使用A标签跳转的化，会导致视图刷新，而只做视图跳转而不是外部链接的化，建议使用Router中专门的组件Link和NavLink。</p>
<h3 id="Link"><a href="#Link" class="headerlink" title="Link"></a>Link</h3><p>Link专门用来做跳转链接，其to属性定义的是单击后要跳转的链接地址。to的属性值有两种形式：字符串和对象。</p>
<p>1、当to属性为字符串时，链接地址的编写和正常URL没有区别；</p>
<p>2、当to的属性为对象时，则链接地址的编写需要放入对象的相关属性中：</p>
<figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&lt;Link </span><br><span class="line">    to=&#123;&#123;</span><br><span class="line">        pathname: <span class="string">"/about"</span>,</span><br><span class="line">        search: <span class="string">"?search=search值"</span>,</span><br><span class="line">        hash: <span class="string">"#hash值"</span>,</span><br><span class="line">        state: &#123;<span class="attr">info</span>: <span class="string">"要传递的其他信息"</span>&#125;</span><br><span class="line">    &#125;&#125;</span><br><span class="line">    &gt;About&lt;<span class="regexp">/Link&gt;</span></span><br></pre></td></tr></table></figure>

<p>这几个属性的作用分别为：pathname代表链接路径的字符串；search代表URL中的search值，hash代表URL中的hash值；state代表要传递给视图的其他信息。</p>
<h3 id="NavLink"><a href="#NavLink" class="headerlink" title="NavLink"></a>NavLink</h3><p>NavLink是一种特殊的Link，一般用作导航中的Link，格式与Link组件一致，且可制作导航的选中效果。在NavLink中有activeClassName、activeStyle、isActive三个属性来进行选中的设置。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React, &#123; Fragment &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; Route, NavLink, useLocation &#125; <span class="keyword">from</span> <span class="string">'react-router-dom'</span>;</span><br><span class="line"><span class="keyword">const</span> navData = [</span><br><span class="line">  &#123;</span><br><span class="line">      pathName:<span class="string">"/"</span>,</span><br><span class="line">      style:&#123;<span class="attr">color</span>:<span class="string">"red"</span>&#125;,</span><br><span class="line">      title:<span class="string">"首页"</span>,</span><br><span class="line">      render()&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>首页视图内容<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">      &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    pathName:<span class="string">"/about"</span>,</span><br><span class="line">    style:&#123;<span class="attr">color</span>:<span class="string">"red"</span>&#125;,</span><br><span class="line">    title:<span class="string">"关于我们"</span>,</span><br><span class="line">    render()&#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>关于我们视图内容<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">];</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Nav</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">    <span class="keyword">let</span> &#123;pathname&#125; = useLocation();</span><br><span class="line">    <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">nav</span>&gt;</span></span></span><br><span class="line"><span class="xml">        &#123;navData.map(item=&gt;&#123;</span></span><br><span class="line">            return &lt;NavLink </span><br><span class="line">              key=&#123;item.pathName&#125;</span><br><span class="line">              to=&#123;item.pathName&#125; </span><br><span class="line">              activeStyle=&#123;item.style&#125;</span><br><span class="line">              isActive=&#123;()=&gt;&#123;</span><br><span class="line">                  return pathname === item.pathName;</span><br><span class="line">              &#125;&#125;</span><br><span class="line"><span class="xml">            &gt;&#123;item.title&#125;<span class="tag">&lt;/<span class="name">NavLink</span>&gt;</span></span></span><br><span class="line"><span class="xml">        &#125;)&#125;</span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">nav</span>&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">Fragment</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">Nav</span> /&gt;</span></span></span><br><span class="line"><span class="xml">        &#123;navData.map(item=&gt;&#123;</span></span><br><span class="line"><span class="xml">            return <span class="tag">&lt;<span class="name">Route</span> <span class="attr">key</span>=<span class="string">&#123;item.pathName&#125;</span> <span class="attr">path</span>=<span class="string">&#123;item.pathName&#125;</span> <span class="attr">exact</span> <span class="attr">render</span>=<span class="string">&#123;item.render&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="xml">        &#125;)&#125;</span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">Fragment</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> App;</span><br></pre></td></tr></table></figure>

<h3 id="404"><a href="#404" class="headerlink" title="404"></a>404</h3><p>404视图：当用户访问一些不存在的URL时返回404视图。一般使用Switch组件包裹Route，当一项匹配成功后，就不再匹配后续。这样可以把要匹配的内容写在Switch组件中，最后一项写404视图，当其他都不匹配成功时，就返回404视图。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React, &#123; Fragment &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; Route, Switch &#125; <span class="keyword">from</span> <span class="string">'react-router-dom'</span>;</span><br><span class="line"><span class="keyword">import</span> Nav <span class="keyword">from</span> <span class="string">'./component/nav'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;routes&#125; <span class="keyword">from</span> <span class="string">"./route/router"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">Fragment</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">Nav</span> /&gt;</span></span></span><br><span class="line">      &lt;Switch&gt;</span><br><span class="line">          &#123;routes.map(item=&gt;&#123;</span><br><span class="line">              return &lt;Route </span><br><span class="line">                  key=&#123;item.path&#125;</span><br><span class="line">                  path=&#123;item.path&#125;</span><br><span class="line">                  exact=&#123;item.exact&#125;</span><br><span class="line">                  render=&#123;item.render&#125;</span><br><span class="line">              /&gt;</span><br><span class="line">          &#125;)&#125;</span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">Switch</span>&gt;</span></span></span><br><span class="line"><span class="xml">  <span class="tag">&lt;/<span class="name">Fragment</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> App;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*route.js*/</span></span><br><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">"react"</span>;</span><br><span class="line"><span class="keyword">import</span> IndexView <span class="keyword">from</span> <span class="string">"../view/index"</span>;</span><br><span class="line"><span class="keyword">import</span> AboutView <span class="keyword">from</span> <span class="string">"../view/about"</span>;</span><br><span class="line"><span class="keyword">import</span> ListView <span class="keyword">from</span> <span class="string">"../view/list"</span>;</span><br><span class="line"><span class="keyword">import</span> UndefinedView <span class="keyword">from</span> <span class="string">"../view/404"</span>;</span><br><span class="line"><span class="keyword">let</span> routes=[&#123;</span><br><span class="line">    path:<span class="string">"/"</span>,</span><br><span class="line">    exact: <span class="literal">true</span>,</span><br><span class="line">    render(props)&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">IndexView</span> &#123;<span class="attr">...props</span>&#125;/&gt;</span></span></span><br><span class="line">    &#125;   </span><br><span class="line">&#125;,&#123;</span><br><span class="line">    path:<span class="string">"/about"</span>,</span><br><span class="line">    exact: <span class="literal">true</span>,</span><br><span class="line">    render(props)&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">AboutView</span> &#123;<span class="attr">...props</span>&#125;/&gt;</span></span></span><br><span class="line">    &#125;   </span><br><span class="line">&#125;,&#123;</span><br><span class="line">    path:[<span class="string">"/list"</span>,<span class="string">"/list/:page"</span>],</span><br><span class="line">    exact: <span class="literal">true</span>,</span><br><span class="line">    render(props)&#123;</span><br><span class="line">        <span class="keyword">let</span> &#123;page=<span class="number">1</span>&#125; = props.match.params;</span><br><span class="line">        <span class="comment">// 解构页码，如果没有传递页面设置默认值为 1。</span></span><br><span class="line">        <span class="keyword">if</span>(page &gt;= <span class="number">1</span>)&#123;</span><br><span class="line">            <span class="comment">// 判断页面是否为 &gt; 1 的数字，如 /list/a 等不是数字的情况下，则显示 404 视图</span></span><br><span class="line">            <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">ListView</span> &#123;<span class="attr">...props</span>&#125;/&gt;</span></span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">UndefinedView</span> &#123;<span class="attr">...props</span>&#125;/&gt;</span></span></span><br><span class="line">    &#125;   </span><br><span class="line">&#125;,&#123;</span><br><span class="line">    path:<span class="string">""</span>,</span><br><span class="line">    exact: <span class="literal">false</span>,</span><br><span class="line">    render(props)&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="xml"><span class="tag">&lt;<span class="name">UndefinedView</span> &#123;<span class="attr">...props</span>&#125;/&gt;</span></span></span><br><span class="line">    &#125;   </span><br><span class="line">&#125;];</span><br><span class="line"><span class="keyword">let</span> navs = [</span><br><span class="line">    &#123;</span><br><span class="line">        to:<span class="string">"/"</span>,</span><br><span class="line">        exact: <span class="literal">true</span>,</span><br><span class="line">        title:<span class="string">"首页"</span>  </span><br><span class="line">    &#125;,&#123;</span><br><span class="line">        to:<span class="string">"/about"</span>,</span><br><span class="line">        exact: <span class="literal">true</span>,</span><br><span class="line">        title: <span class="string">"关于"</span>   </span><br><span class="line">    &#125;,&#123;</span><br><span class="line">        to:<span class="string">"/list"</span>,</span><br><span class="line">        title: <span class="string">"课程列表"</span>,</span><br><span class="line">        isActive(url)&#123;</span><br><span class="line">            <span class="keyword">let</span> urlData = url.split(<span class="string">"/"</span>); </span><br><span class="line">            <span class="keyword">if</span>(url === <span class="string">"/list"</span></span><br><span class="line">            ||(urlData.length===<span class="number">3</span>&amp;&amp;urlData[<span class="number">1</span>] === <span class="string">"list"</span>&amp;&amp;urlData[<span class="number">2</span>]&gt;<span class="number">0</span>) )&#123;</span><br><span class="line">                <span class="comment">// 判断 URL 为 "/list" 或 "/list/大于1的数字" 时，选中当前项，否则不选中 </span></span><br><span class="line">                <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">            &#125;   </span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;   </span><br><span class="line">    &#125;</span><br><span class="line">];</span><br><span class="line"><span class="keyword">export</span> &#123;routes,navs&#125;;</span><br></pre></td></tr></table></figure>


      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/11/react/react%E4%BF%AE%E7%82%BC(%E4%BA%8C)-Redux/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/11/react/react%E4%BF%AE%E7%82%BC(%E4%BA%8C)-Redux/" class="post-title-link" itemprop="url">react修炼（二）-Redux</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              
    
              <time title="创建时间：2021-11-11 12:18:39 / 修改时间：23:47:34" itemprop="dateCreated datePublished" datetime="2021-11-11T12:18:39+08:00">2021-11-11</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/react/" itemprop="url" rel="index"><span itemprop="name">react</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>React组件的更新极其依赖于组件的状态，而组件嵌套多层时，则可能会把父级的状态一层一层往下传递，这样的话再管理和使用都极其不便。Redux是JavaScript的状态容器，提供可预测化的状态管理，在React中使用Redux，可以把所有的state集中到组件顶部，能够灵活地将所有state分发给所有的组件，这样就极大方便开发者管理React中的状态，也方便不同组件间的通信。</p>
<h1 id="Redux使用"><a href="#Redux使用" class="headerlink" title="Redux使用"></a>Redux使用</h1><p>Reudx并不依赖于任何库，因此除了和React一起使用以外，也可以搭配其他视图库一起使用；在Redux中有几个核心概念：1、store：一个数据容器，用来管理和保存这个项目的state，在整个应用中只能有一个store；2、state：一个对象，在state中存储相应的数据，则可以通过store提供的方法来获取state；3、action：一个通知命令，用于对state进行修改。通过store提供的方法，可以发起action完成对state的修改。</p>
<h2 id="API"><a href="#API" class="headerlink" title="API"></a>API</h2><h3 id="Reducer"><a href="#Reducer" class="headerlink" title="Reducer"></a>Reducer</h3><p>action是一个通知命令，本质上是JavaScript普通对象，在action中，必须包含一个字符串类型的type属性；该属性值代表要对state做何种操作。除了type属性外，开发者可以根据业务需求定义action的其他属性，最终action会通过store传入reducer函数中以完成对state的修改。</p>
<p><img src="/../../image/react%E4%BF%AE%E7%82%BC(%E4%BA%8C)-Redux/MMA%7D1PBV9D1%5DLA%25%25P4OMJ67.png" alt="img"></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;createStore&#125; <span class="keyword">from</span> <span class="string">'redux'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reducer</span>(<span class="params">state=&#123;&#125;, action</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> state;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">let</span> store = createStore(reducer);</span><br><span class="line"><span class="comment">//利用createStore方法可以创建项目中的store</span></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">利用createStore方法创建store时，一定要调入reducer函数，该函数的名字可以根据语义自行定义，该函数会帮助开发者初始化项目的state</span></span><br><span class="line"><span class="comment">后期对state的修改也都会在该函数中进行。</span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure>

<p>Redux中所有的数据都被保存在同一个state对象中，通过store可以发起action修改命令对state进行修改。当发起修改执行时，store会把state和action传入reducer函数中，reducer接收到action后可以根据action的指令对state进行修改，并返回修改后的state。一个完整的reducer函数如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reducer</span>(<span class="params">state=&#123;</span></span></span><br><span class="line"><span class="function"><span class="params">    count: <span class="number">1</span><span class="regexp">//</span>在这里设置state的初始值</span></span></span><br><span class="line"><span class="function"><span class="params">&#125;, action</span>) </span>&#123;</span><br><span class="line">    <span class="comment">//action.type代表修改指令，该指令可以自定义</span></span><br><span class="line">    <span class="keyword">switch</span>(action.type) &#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"COUNT_PLUS"</span>:</span><br><span class="line">            <span class="comment">//根据action指令，修改state并返回新的state</span></span><br><span class="line">            <span class="keyword">return</span> &#123;<span class="attr">count</span>: state.count + <span class="number">1</span>&#125;;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"COUNT_REDUCE"</span>:</span><br><span class="line">            <span class="keyword">return</span> &#123;<span class="attr">count</span>: state.count - <span class="number">1</span>&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> state;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>reducer本质就是一个函数，该函数有两个参数state和action，另外该函数一定有返回值，返回值时修改后新得state，在编写reducer时，一定要注意它是一个纯函数。</p>
<p>纯函数：1、该函数的执行过程中无任何副作用，如:网络请求、DOM操作、定时器；2、如果函数的调用参数相同，则永远返回相同的结果，并不依赖于程序执行期间外部任何状态或数据的变化。必须只依赖于其输入参数。 </p>
<h3 id="Store"><a href="#Store" class="headerlink" title="Store"></a>Store</h3><p>store是保存数据的容器，是createStore生成的一个对象，而store对象提供的方法有：1、getState方法：该方法用于获取state；2、dispatch(action):该方法用于发起一个action；3、subscribe(listener):该方法会注册一个监听器监听state发生的变化，另外该方法的返回值会返回一个注销监听器的方法，用于取消监听器。这些方法的操作说明如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> ReactDOM <span class="keyword">from</span> <span class="string">'react-dom'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;createStore&#125; <span class="keyword">from</span> <span class="string">'redux'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reducer</span>(<span class="params">state=&#123;</span></span></span><br><span class="line"><span class="function"><span class="params">    count: <span class="number">1</span>,<span class="regexp">//</span>在这里设置state的初始值</span></span></span><br><span class="line"><span class="function"><span class="params">&#125;, action</span>) </span>&#123;</span><br><span class="line">    <span class="comment">//action.type代表修改指令，该指令可以自定义</span></span><br><span class="line">    <span class="keyword">switch</span>(action.type) &#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"COUNT_PLUS"</span>:</span><br><span class="line">            <span class="comment">//根据action指令，修改state并返回新的state</span></span><br><span class="line">            <span class="keyword">return</span> &#123;<span class="attr">count</span>: state.count + <span class="number">1</span>&#125;;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"COUNT_REDUCE"</span>:</span><br><span class="line">            <span class="keyword">return</span> &#123;<span class="attr">count</span>: state.count - <span class="number">1</span>&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> state;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">let</span> state = store.getState();</span><br><span class="line">    ReactDOM.render(</span><br><span class="line">    	&lt;div&gt;</span><br><span class="line">        &lt;button onClick=&#123;() =&gt; &#123;</span><br><span class="line">        	store.dispatch(&#123;<span class="attr">type</span>: <span class="string">"COUNT_REDUCE"</span>&#125;);</span><br><span class="line">    	&#125;&#125;&gt;-&lt;<span class="regexp">/button&gt;</span></span><br><span class="line"><span class="regexp">		&lt;span&gt;&#123;state.count&#125;&lt;/</span>span&gt;</span><br><span class="line">		&lt;button onClick=&#123;() =&gt; &#123;</span><br><span class="line">            store.dispatch(&#123;<span class="attr">type</span>: <span class="string">"COUNT_PLUS"</span>&#125;);</span><br><span class="line">        &#125;&#125;&gt;+&lt;<span class="regexp">/button&gt;</span></span><br><span class="line"><span class="regexp">		&lt;/</span>div&gt;, <span class="built_in">document</span>.getElementById(<span class="string">'root'</span>);</span><br><span class="line">    );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> store = createStore(reducer);</span><br><span class="line"><span class="comment">//添加监听，当state发生变化时，重新渲染视图</span></span><br><span class="line"><span class="keyword">let</span> unSubscribe = store.subscribe(render);</span><br><span class="line"><span class="comment">//完成初次视图渲染</span></span><br><span class="line">render();</span><br><span class="line"><span class="comment">//10秒后取消对state变化的监听</span></span><br><span class="line">setTimeout(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    unSubscribe();</span><br><span class="line">&#125;, <span class="number">10000</span>);</span><br></pre></td></tr></table></figure>

<p>在上述代码中，可以看到Redux的基本流程：基于reducer创建store，从store中获取state并传递给视图，当视图被操作时，通过dispatch发起一个action，store接收到action之后，会把state和action传递给reducer，reducer更新state并把新得state传递给视图进行更新。</p>
<p><img src="/../../image/react%E4%BF%AE%E7%82%BC(%E4%BA%8C)-Redux/image-20211111113443814.png" alt="image-20211111113443814"></p>
<h2 id="React-Redux"><a href="#React-Redux" class="headerlink" title="React-Redux"></a>React-Redux</h2><p>直接在React中去使用Redux会很麻烦，组件嵌套层级一多，就需要一层层传递store。React-Redux库则可以帮助开发者便捷地使用Redux。使用步骤如下：</p>
<p>1、新建src/store.js并写下以下代码：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;createStore&#125; <span class="keyword">from</span> <span class="string">'redux'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">countFn</span>(<span class="params">state=&#123;</span></span></span><br><span class="line"><span class="function"><span class="params">    count:<span class="number">1</span></span></span></span><br><span class="line"><span class="function"><span class="params">&#125;, action</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">switch</span>(action.type) &#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"COUNT_PLUS"</span>:</span><br><span class="line">            <span class="keyword">return</span> &#123; <span class="attr">count</span>: state.count + <span class="number">1</span>&#125;;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"COUNT_REDUCE"</span>:</span><br><span class="line">            <span class="keyword">return</span> &#123; <span class="attr">count</span>: state.count - <span class="number">1</span>&#125;;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> state;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">let</span> store = createStore(countFn);</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> store;</span><br></pre></td></tr></table></figure>

<p>2、创建好store之后，就可以通过React-Redux把store关联到项目中，React-Redux中提供了一个组件Provider，该组件就是其前文中说过的context中的Provider，它的作用就是向其后代子孙传递信息。在Provider中有一个store属性，该属性的属性值是创建好的store。另外需要注意：Redux提倡一个项目只有一个store，所以Provider应该在整个项目的最外层。修改src/index.js的代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> ReactDOM <span class="keyword">from</span> <span class="string">'react-dom'</span>;</span><br><span class="line"><span class="keyword">import</span> App <span class="keyword">from</span> <span class="string">'./App'</span>;</span><br><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">"./store"</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;Provider&#125; <span class="keyword">from</span> <span class="string">"react-redux"</span>;</span><br><span class="line">ReactDOM.render(</span><br><span class="line">&lt;Provider store=&#123;store&#125;&gt;</span><br><span class="line">    &lt;App /&gt;</span><br><span class="line">&lt;<span class="regexp">/Provider&gt;,</span></span><br><span class="line"><span class="regexp">document.getElementById('root'));</span></span><br></pre></td></tr></table></figure>

<h3 id="connect"><a href="#connect" class="headerlink" title="connect"></a>connect</h3><p>通过Provider已经可以把Redux的store传递给项目中的各个组件，那在组件中2如何接收和修改store中的state呢？React-Redux中提供了一个connect方法用于接收Provider传递下来的store中的state和dispatch。</p>
<p>connect (state =&gt; newProps)(Component);</p>
<p>这是connect比较常见的写法，该代码含义为调用connect时，传递一个回调函数给connect，connect会把store中的state传递给这个回调函数。而在回调函数中，找出组件需要的部分，然后返回，这是connect (state =&gt; newProps)这段代码的意思。除此之外，connect方法还会返回一个函数，该函数接收一个参数，这个参数是要获取state或dispatch的组件。</p>
<p>该函数会调用传递进去的组件，并把connect回调函数执行后的返回值，以及dispatch方法和父组件传递进来的数据一块传递给该组件。另外，该函数执行完成之后会返回一个新的组件以供父级调用。修改aec/App.js的代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;connect&#125; <span class="keyword">from</span> <span class="string">"react-redux"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">let</span> &#123;count, dispatch&#125; = props;</span><br><span class="line">    <span class="keyword">return</span> (<span class="xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line">           &lt;button onClick=&#123;() =&gt; &#123;</span><br><span class="line">        dispatch(&#123;type: "COUNT_REDUCE"&#125;);</span><br><span class="line"><span class="xml">    &#125;&#125;&gt;-<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="xml">	<span class="tag">&lt;<span class="name">span</span>&gt;</span>&#123;count&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line">	&lt;button onClick=&#123;() =&gt; &#123;</span><br><span class="line">        dispatch(&#123;type: "COUNT_PLUS"&#125;);</span><br><span class="line"><span class="xml">    &#125;&#125;&gt;+<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="xml">	<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">	);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">App = connect(<span class="function"><span class="params">state</span> =&gt;</span> state) (App);</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> App;</span><br></pre></td></tr></table></figure>

<h3 id="Hooks"><a href="#Hooks" class="headerlink" title="Hooks"></a>Hooks</h3><p>React-redux也新增加了hooks，其Hooks也只能在React函数组件或自定义Hooks中使用。</p>
<p>1、const state = useSelector(state =&gt; state)与connect类时，useSelector接收一个回调函数，该回调函数会被useSelector调用，并且会把store中的state传递给该回调函数。该回调函数必须有返回值，该返回值一般是在state中开发者想要使用的数据，该数据最终会被useSelector返回；</p>
<p>2、const dispatch = useDispatch()；调用useDispatch，Hook返回值为Redux的dispatch方法；</p>
<p>3、const store = useStore()；调用useStore，Hook会返回Redux的store。使用React-Redux的Hooks，对计数器的案例进行一个修改，示例代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;useSelector, useDispach&#125; <span class="keyword">from</span> <span class="string">"react-redux"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params">props</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> count = useSelector(<span class="function"><span class="params">state</span>=&gt;</span>state.count);</span><br><span class="line">  <span class="keyword">const</span> dispatch = useDispatch();</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;div&gt;</span><br><span class="line">      &lt;button onClick=&#123;()=&gt;&#123;</span><br><span class="line">          dispatch(&#123;</span><br><span class="line">            type:<span class="string">"COUNT_REDUCE"</span></span><br><span class="line">          &#125;);</span><br><span class="line">        &#125;&#125;&gt;-&lt;<span class="regexp">/button&gt;</span></span><br><span class="line"><span class="regexp">        &lt;span&gt;&#123;count&#125;&lt;/</span>span&gt;</span><br><span class="line">        &lt;button onClick=&#123;()=&gt;&#123;</span><br><span class="line">          dispatch(&#123;</span><br><span class="line">            type:<span class="string">"COUNT_PLUS"</span></span><br><span class="line">          &#125;);</span><br><span class="line">        &#125;&#125;&gt;+&lt;<span class="regexp">/button&gt;  </span></span><br><span class="line"><span class="regexp">    &lt;/</span>div&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> App;</span><br></pre></td></tr></table></figure>

<h2 id="Todos"><a href="#Todos" class="headerlink" title="Todos"></a>Todos</h2><p>下面我们简单地实现一个todos来对上面的React Hooks和Redux做一个实践。</p>
<h3 id="index-js"><a href="#index-js" class="headerlink" title="index.js"></a>index.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//index.js是整个文件的入口</span></span><br><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> ReactDOM <span class="keyword">from</span> <span class="string">'react-dom'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;Provider&#125; <span class="keyword">from</span> <span class="string">"react-redux"</span>;</span><br><span class="line"><span class="keyword">import</span> App <span class="keyword">from</span> <span class="string">"./App"</span>;</span><br><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">"./store"</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">"./index.css"</span>;</span><br><span class="line">ReactDOM.render(</span><br><span class="line">    &lt;Provider store=&#123;store&#125;&gt;</span><br><span class="line">        &lt;App /&gt;</span><br><span class="line">    &lt;<span class="regexp">/Provider&gt;, </span></span><br><span class="line"><span class="regexp">    document.getElementById('root')</span></span><br><span class="line"><span class="regexp">);</span></span><br><span class="line"><span class="regexp">/</span>*</span><br><span class="line">在index.js中引入React-Redux，把store关联进整个项目，并用Provider包裹代码，提供数据上下文环境。</span><br><span class="line">*<span class="regexp">/</span></span><br></pre></td></tr></table></figure>

<h3 id="store-js"><a href="#store-js" class="headerlink" title="store.js"></a>store.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//调用createStore，传入reducer从而创建store对象；</span></span><br><span class="line"><span class="comment">//在index.js中，用Provider把Redux的store分发给项目的各个组件</span></span><br><span class="line"><span class="keyword">import</span> &#123;createStore&#125; <span class="keyword">from</span> <span class="string">"redux"</span>;</span><br><span class="line"><span class="keyword">import</span> reducer <span class="keyword">from</span> <span class="string">"./reducer"</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> createStore(reducer);</span><br></pre></td></tr></table></figure>

<h3 id="reducer-js"><a href="#reducer-js" class="headerlink" title="reducer.js"></a>reducer.js</h3><p>在数据关联视图之前，先建立好数据模型，由于Todos中记录的是多项数据，所以使用数组保存相关数据。且每一项数据有两种状态以及待办事项的文字，所以每一项数据中记录三个字段：id(数据ID)、title(该数据的名字)、done(是否完成)。</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">state = [</span><br><span class="line">    &#123;</span><br><span class="line">        id : 0,</span><br><span class="line">        title : "示例数据",</span><br><span class="line">        done : true</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">        id : 1,</span><br><span class="line">        title : "示例数据2",</span><br><span class="line">        done : true</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">        id : 2,</span><br><span class="line">        title : "示例数据3",</span><br><span class="line">        done : false</span><br><span class="line">    &#125;,</span><br><span class="line">]</span><br></pre></td></tr></table></figure>

<p>有了数据模型后，我们再去创建store，在建立reducer时，我们也可以预测以下以后会发生的action，对reducer进行进一步完善。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*在reducer中，我们先想好并定义该案例中可能会执行的操作</span></span><br><span class="line"><span class="comment">在reducer中注意，最好每一次返回一个新对象</span></span><br><span class="line"><span class="comment">react部分特性在组件更新时候进行浅对比，如果返回的是一个原有对象，经常会造成组件不更新。</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reducer</span>(<span class="params">state=[],action</span>)</span>&#123;</span><br><span class="line">    <span class="keyword">switch</span>(action.type)&#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"TODO_ADD"</span>:</span><br><span class="line">            <span class="keyword">return</span> [</span><br><span class="line">                ...state,</span><br><span class="line">                &#123;</span><br><span class="line">                    id: <span class="built_in">Date</span>.now(),</span><br><span class="line">                    title: action.title,</span><br><span class="line">                    done: <span class="literal">false</span></span><br><span class="line">                &#125;</span><br><span class="line">            ]</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"TODO_DONE"</span>:</span><br><span class="line">            state.forEach(<span class="function"><span class="params">item</span>=&gt;</span>&#123;</span><br><span class="line">                <span class="keyword">if</span>(item.id === action.id)&#123;</span><br><span class="line">                    item.done = action.done </span><br><span class="line">                &#125;</span><br><span class="line">            &#125;)  </span><br><span class="line">            <span class="keyword">return</span> [...state];</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"TODO_EDIT"</span>:</span><br><span class="line">            state.forEach(<span class="function"><span class="params">item</span>=&gt;</span>&#123;</span><br><span class="line">                <span class="keyword">if</span>(item.id === action.id)&#123;</span><br><span class="line">                    item.title = action.title; </span><br><span class="line">                &#125;</span><br><span class="line">            &#125;)  </span><br><span class="line">            <span class="keyword">return</span> [...state];</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"TODO_REMOVE"</span>:</span><br><span class="line">            state=state.filter(<span class="function"><span class="params">item</span>=&gt;</span>item.id!==action.id);  </span><br><span class="line">            <span class="keyword">return</span> state;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"TODO_REMOVE_DONE"</span>:</span><br><span class="line">            state=state.filter(<span class="function"><span class="params">item</span>=&gt;</span>(!item.done));</span><br><span class="line">            <span class="keyword">return</span> state;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> state;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> reducer;</span><br></pre></td></tr></table></figure>

<h3 id="App-js"><a href="#App-js" class="headerlink" title="App.js"></a>App.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*App.js是项目的起始点，在App.js中Todos和State两个组件要根据数据来决定是否显示</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">import</span> React, &#123; Fragment &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> Title <span class="keyword">from</span> <span class="string">'./title'</span>;</span><br><span class="line"><span class="keyword">import</span> Create <span class="keyword">from</span> <span class="string">'./create'</span>;</span><br><span class="line"><span class="keyword">import</span> Todos <span class="keyword">from</span> <span class="string">'./todos'</span>;</span><br><span class="line"><span class="keyword">import</span> State <span class="keyword">from</span> <span class="string">'./state'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useSelector &#125; <span class="keyword">from</span> <span class="string">'react-redux'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">App</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">    <span class="keyword">let</span> data = useSelector(<span class="function"><span class="params">state</span>=&gt;</span>state);</span><br><span class="line">    <span class="keyword">return</span> (<span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">"todoapp"</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">Title</span> /&gt;</span></span></span><br><span class="line">        &lt;div className="content"&gt;</span><br><span class="line">            &lt;Create /&gt;</span><br><span class="line">            &#123;</span><br><span class="line">              data.length&gt;0?&lt;Fragment&gt;</span><br><span class="line">                &lt;Todos /&gt;</span><br><span class="line">                &lt;State /&gt;</span><br><span class="line">              &lt;/Fragment&gt;</span><br><span class="line">              :</span><br><span class="line">              ""</span><br><span class="line">            &#125;</span><br><span class="line"><span class="xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> App;</span><br></pre></td></tr></table></figure>

<h3 id="create-js"><a href="#create-js" class="headerlink" title="create.js"></a>create.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">1、在create.js中把输入空设置为受控组件，以便获取输入内容，其实就是Vue的双向绑定；</span></span><br><span class="line"><span class="comment">2、添加ketdown事件，在键盘按下时，如果按下的是Enter键并且此时输入框不为空，则发起一个action添加一项todo</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">import</span> React, &#123; useState &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useDispatch &#125; <span class="keyword">from</span> <span class="string">'react-redux'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Create</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">    <span class="keyword">const</span> [val,setVal] = useState(<span class="string">""</span>);</span><br><span class="line">    <span class="keyword">const</span> dispatch = useDispatch();</span><br><span class="line">    <span class="keyword">return</span> (<span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">"create-todo"</span>&gt;</span></span></span><br><span class="line">        &lt;input </span><br><span class="line">          id="new-todo" </span><br><span class="line">          placeholder="请输入要完成的事项" </span><br><span class="line">          autoComplete="off"</span><br><span class="line">          type="text"</span><br><span class="line">          value=&#123;val&#125;</span><br><span class="line">          onChange=&#123;(&#123;target&#125;)=&gt;&#123;</span><br><span class="line">              setVal(target.value);</span><br><span class="line">          &#125;&#125;</span><br><span class="line">          onKeyDown=&#123;(&#123;keyCode&#125;)=&gt;&#123;</span><br><span class="line">            if(keyCode === 13</span><br><span class="line">            &amp;&amp; val.trim())&#123;</span><br><span class="line">                dispatch(&#123;</span><br><span class="line">                  type: "TODO_ADD",</span><br><span class="line">                  title:val</span><br><span class="line">                &#125;);</span><br><span class="line">                setVal("")</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;&#125;</span><br><span class="line"><span class="xml">        /&gt;</span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> Create;</span><br></pre></td></tr></table></figure>

<h3 id="todo-js"><a href="#todo-js" class="headerlink" title="todo.js"></a>todo.js</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">在Todos中，根据当前的Todo数量来生成每一项todo，并把数据传递给对应的todo。</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> Li <span class="keyword">from</span> <span class="string">'./li'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useSelector &#125; <span class="keyword">from</span> <span class="string">'react-redux'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Todos</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">let</span> data = useSelector(<span class="function"><span class="params">state</span>=&gt;</span>state);</span><br><span class="line">    <span class="keyword">return</span> (<span class="xml"><span class="tag">&lt;<span class="name">ul</span> <span class="attr">id</span>=<span class="string">"todo-list"</span>&gt;</span></span></span><br><span class="line">        &#123;data.map(item=&gt;(&lt;Li </span><br><span class="line">            key=&#123;item.id&#125;</span><br><span class="line">            &#123;...item&#125;</span><br><span class="line"><span class="xml">        /&gt;))&#125;</span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> Todos;</span><br></pre></td></tr></table></figure>

<h3 id="li-js"><a href="#li-js" class="headerlink" title="li.js"></a>li.js</h3><p>Li组件中要做的事比较多，按照功能分为如下：</p>
<p>1、从props中解构出id、title、done；先把视图中显示的文字进行修改，再将复选框改为受控组件，并和done进行关联。</p>
<p>2、给destroy添加单击事件，单击时发起action删除本项todo。</p>
<p>3、添加编辑功能，基本上分为如下步骤：(1)：声明state，用于控制是否进入编辑状态    。当双击文本时进入编辑状态，当输入框失去焦点时，则退出编辑状态；</p>
<p>(2)：此时进入编辑状态时输入框没有获得焦点，则点击页面的其他区域时也不会触发输入框的失焦效果，这样可能会导致多个todo进入编辑状态。为解决这个问题，我们这里使用ref和effect，将声明ref和输入框进行绑定，声明effect检测编辑状态改变。当编辑状态改变，并且是进入编辑状态时，让输入框获得焦点。这样单击页面其他区域则会触发blur事件，让todo退出触发状态。</p>
<p>(3)：完成编辑功能：这里药注意编辑输入框的值，不能直接和title进行绑定做成受控组件。如果用户清空了输入框的值，则退出编辑状态，title也会变成空的。正确的做法是声明一个state，让state的初始值为title，输入框和该state绑定。退出编辑时，判断state是否为空，若为空则让state恢复为title，否则再对state进行修改。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React, &#123; useState, useRef, useEffect &#125; <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useDispatch &#125; <span class="keyword">from</span> <span class="string">'react-redux'</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Li</span> (<span class="params">props</span>)</span>&#123;</span><br><span class="line">    <span class="keyword">let</span> &#123;id,title,done&#125; = props;</span><br><span class="line">    <span class="keyword">const</span> [isEdit,changeEdit] = useState(<span class="literal">false</span>);</span><br><span class="line">    <span class="keyword">const</span> dispatch = useDispatch();</span><br><span class="line">    <span class="keyword">let</span> edit = useRef();</span><br><span class="line">    <span class="keyword">let</span> [val,setVal] = useState(title);</span><br><span class="line">    useEffect(<span class="function"><span class="params">()</span>=&gt;</span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(isEdit)&#123;</span><br><span class="line">            edit.current.focus();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,[isEdit]);</span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">        &lt;li className=&#123;isEdit?<span class="string">"editing"</span>:<span class="string">""</span>&#125;&gt;</span><br><span class="line">            &lt;div className=&#123;<span class="string">"todo "</span>+(done?<span class="string">"done"</span>:<span class="string">""</span>)&#125;&gt;</span><br><span class="line">                &lt;div className=<span class="string">"display"</span>&gt;</span><br><span class="line">                    &lt;input </span><br><span class="line">                        className=<span class="string">"check"</span> </span><br><span class="line">                        type=<span class="string">"checkbox"</span></span><br><span class="line">                        checked=&#123;done&#125;</span><br><span class="line">                        onChange=&#123;(&#123;target&#125;)=&gt;&#123;</span><br><span class="line">                            dispatch(&#123;</span><br><span class="line">                                type:<span class="string">"TODO_DONE"</span>,</span><br><span class="line">                                id,</span><br><span class="line">                                done:target.checked</span><br><span class="line">                            &#125;)</span><br><span class="line">                        &#125;&#125;</span><br><span class="line">                    /&gt;</span><br><span class="line">                    &lt;div </span><br><span class="line">                        className=<span class="string">"todo-content"</span></span><br><span class="line">                        onDoubleClick=&#123;()=&gt;&#123;</span><br><span class="line">                            changeEdit(<span class="literal">true</span>);</span><br><span class="line">                        &#125;&#125;</span><br><span class="line">                    &gt;&#123;title&#125;&lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">                    &lt;span </span></span><br><span class="line"><span class="regexp">                        className="todo-destroy"</span></span><br><span class="line"><span class="regexp">                        onClick=&#123;()=&gt;&#123;</span></span><br><span class="line"><span class="regexp">                            dispatch(&#123;</span></span><br><span class="line"><span class="regexp">                                type:"TODO_REMOVE",</span></span><br><span class="line"><span class="regexp">                                id</span></span><br><span class="line"><span class="regexp">                            &#125;)</span></span><br><span class="line"><span class="regexp">                        &#125;&#125;</span></span><br><span class="line"><span class="regexp">                    &gt;&lt;/</span>span&gt;</span><br><span class="line">                &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">                &lt;div className="edit"&gt;</span></span><br><span class="line"><span class="regexp">                    &lt;input </span></span><br><span class="line"><span class="regexp">                        className="todo-input" </span></span><br><span class="line"><span class="regexp">                        type="text"</span></span><br><span class="line"><span class="regexp">                        ref=&#123;edit&#125;</span></span><br><span class="line"><span class="regexp">                        value=&#123;val&#125;</span></span><br><span class="line"><span class="regexp">                        onChange=&#123;(&#123;target&#125;)=&gt;&#123;</span></span><br><span class="line"><span class="regexp">                            setVal(target.value);</span></span><br><span class="line"><span class="regexp">                        &#125;&#125;</span></span><br><span class="line"><span class="regexp">                        onBlur=&#123;()=&gt;&#123;</span></span><br><span class="line"><span class="regexp">                            if(val.trim())&#123;</span></span><br><span class="line"><span class="regexp">                                dispatch(&#123;</span></span><br><span class="line"><span class="regexp">                                    type:"TODO_EDIT",</span></span><br><span class="line"><span class="regexp">                                    id,</span></span><br><span class="line"><span class="regexp">                                    title:val</span></span><br><span class="line"><span class="regexp">                                &#125;)</span></span><br><span class="line"><span class="regexp">                            &#125; else &#123;</span></span><br><span class="line"><span class="regexp">                                setVal(title);</span></span><br><span class="line"><span class="regexp">                            &#125;</span></span><br><span class="line"><span class="regexp">                            changeEdit(false);</span></span><br><span class="line"><span class="regexp">                        &#125;&#125;</span></span><br><span class="line"><span class="regexp">                    /</span>&gt;</span><br><span class="line">                &lt;<span class="regexp">/div&gt;</span></span><br><span class="line"><span class="regexp">            &lt;/</span>div&gt;</span><br><span class="line">        &lt;<span class="regexp">/li&gt;</span></span><br><span class="line"><span class="regexp">    )</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br><span class="line"><span class="regexp">export default Li;</span></span><br></pre></td></tr></table></figure>

<h3 id="state-js"><a href="#state-js" class="headerlink" title="state.js"></a>state.js</h3><p>在State组件中需要获取所有的数据，分别筛选出已完成和未完成的数量同步至视图中。另外单击已完成事项按钮时，发起一个action清空已完成的todo项。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> React <span class="keyword">from</span> <span class="string">'react'</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;useSelector,useDispatch&#125; <span class="keyword">from</span> <span class="string">"react-redux"</span>;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">State</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">    <span class="keyword">let</span> data = useSelector(<span class="function"><span class="params">state</span>=&gt;</span>state);</span><br><span class="line">    <span class="keyword">let</span> unDone = data.filter(<span class="function"><span class="params">item</span>=&gt;</span>(!item.done));</span><br><span class="line">    <span class="keyword">let</span> done = data.filter(<span class="function"><span class="params">item</span>=&gt;</span>item.done);</span><br><span class="line">    <span class="keyword">let</span> dispatch = useDispatch();</span><br><span class="line">    <span class="keyword">return</span> (<span class="xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">"todo-stats"</span>&gt;</span></span></span><br><span class="line">            &lt;span className="todo-count"&gt;</span><br><span class="line">              &lt;span className="number"&gt;&#123;unDone.length&#125;&lt;/span&gt; &lt;span className="word"&gt;项待完成&lt;/span&gt;</span><br><span class="line"><span class="xml">            <span class="tag">&lt;/<span class="name">span</span>&gt;</span> </span></span><br><span class="line"><span class="xml">            &#123;</span></span><br><span class="line"><span class="xml">              done.length&gt;0?</span></span><br><span class="line">              &lt;span className="todo-clear"&gt;</span><br><span class="line">                &lt;a </span><br><span class="line">                  href="#"</span><br><span class="line">                  onClick=&#123;()=&gt;&#123;</span><br><span class="line">                    dispatch(&#123;</span><br><span class="line">                      type:"TODO_REMOVE_DONE"</span><br><span class="line">                    &#125;)</span><br><span class="line">                  &#125;&#125;</span><br><span class="line">                &gt;</span><br><span class="line">                    Clear &lt;span&gt;&#123;done.length&#125;&lt;/span&gt; 已完成事项</span><br><span class="line">                &lt;/a&gt;</span><br><span class="line"><span class="xml">              <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="xml">              :""</span></span><br><span class="line"><span class="xml">            &#125; </span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">    )</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> State;</span><br></pre></td></tr></table></figure>

<h2 id="中间件"><a href="#中间件" class="headerlink" title="中间件"></a>中间件</h2><p>Redux中一个项目只会有一个store，当项目变得越来越庞大时，reducer也会变得极其庞大，会使项目难以维护。为解决这个问题，开发者可以对reducer按照模块进行拆分，这样可以让reducer中的解构更加清晰，也便于维护。例如，现在有一个项目，其中有两个模块的数据：user、todo。reducer应该为以下格式：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reducer</span>(<span class="params">state, action</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">switch</span>(action.type) &#123;</span><br><span class="line">            ...;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        user:&#123;...&#125;,</span><br><span class="line">        todo:[...]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>拆分后的代码结果如下：把user和tode的逻辑放在不同的函数里进行处理：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">user</span>(<span class="params">user=&#123;...&#125;, action</span>)</span>&#123;</span><br><span class="line">        <span class="keyword">switch</span>(action.type) &#123;</span><br><span class="line">    	...</span><br><span class="line">	&#125;</span><br><span class="line">    <span class="keyword">return</span> user;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">todo</span>(<span class="params">todo=&#123;...&#125;, action</span>)</span>&#123;</span><br><span class="line">        <span class="keyword">switch</span>(action.type) &#123;</span><br><span class="line">    	...</span><br><span class="line">	&#125;</span><br><span class="line">    <span class="keyword">return</span> todo;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">reducer</span>(<span class="params">state=&#123;&#125;, action</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        user:user(state.user, action)</span><br><span class="line">        todo:todo(state.todo, action)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在上述代码中，先声明了一个根reducer函数，然后在该函数中拆分出user和todo两个函数。创建store时只需要把reducer函数传递给createStore即可。这样就把不同模块的state管理，放在了不同函数中，将来要做修改时，会极大方便开发者定位代码进行维护。另外在Redux中有一个方法combineReducers(rootReducer)，可以帮助开发者快速合并多个reducer函数。</p>
<p>combineReducers(rootReducer)方法需要传入一个rootReducer对象，它会根据这个对象生成一个reducer方法，帮助开发者合并多个reducer。rootReducer是一个对象，对象的key值是将来生成的state的key值。属性值则是拆分后的各个reducer。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">combineReducers(&#123;</span><br><span class="line">    user: user,</span><br><span class="line">    todo: todo</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<h3 id="redux-thunk"><a href="#redux-thunk" class="headerlink" title="redux-thunk"></a>redux-thunk</h3><p>Redux的核心操作比较简单，基于reducer创建store，利用dispatch发起action，reducer接收到action之后，再完成state的修改。但在实际工作时，发起action之后，到达reducer之前，会有一些额外的工作需要处理，如日志记录、数据请求等这些都属于副作用。而reducer本身是一个纯函数，这时就需要中间件。</p>
<p>middleware中间件：之前dispatch发起action之后，会直接把action传递到reducer中。使用了中间件之后。dispatch发起action之后，会先把action传递给中间件；中间件这时就可以做相关的副作用，处理完之后再由中间件把action传递给reducer。Redux社群中有很多优秀的中间件：日志系统redux-logger、异步中间件redux-saga、异步中间件redux-thunk。</p>
<p>下面是Redux-thunk的示例代码：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">在创建store时，引入中间件。在Redux中提供了applyMiddleware(...middlewares)方法；</span></span><br><span class="line"><span class="comment">项目要引入的中间件都可以传递进applyMiddleware，然后把applyMiddleware的返回值传递给createStore</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">import</span> &#123;createStore, applyMiddleware&#125; <span class="keyword">from</span> <span class="string">"redux"</span>;</span><br><span class="line"><span class="keyword">import</span> reducer <span class="keyword">from</span> <span class="string">"./reducer"</span>;</span><br><span class="line"><span class="keyword">import</span> thunk <span class="keyword">from</span> <span class="string">"redux-thunk"</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">default</span> createStore(reducer, applyMiddleware(thunk));</span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">添加完thunk后，在项目中就可以利用thunk来做中间处理了。使用了thunk之后，dispatch可以接收两种不同类型参数。</span></span><br><span class="line"><span class="comment">参数类型是对象时，不会经过中间件处理，而是把action发送到reducer中。</span></span><br><span class="line"><span class="comment">参数类型是函数时，会执行该函数，以及dispatch和getState两个方法传递给该函数，在函数中可以进行中间处理，如数据请求等；</span></span><br><span class="line"><span class="comment">处理完之后再调用dispatch更新state</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">dispatch(<span class="function"><span class="keyword">function</span>(<span class="params">dispatch, getState</span>)</span>&#123;</span><br><span class="line">    http.get(<span class="string">"..."</span>).then(<span class="function">(<span class="params">res</span>) =&gt;</span> &#123;</span><br><span class="line">        dispatch(&#123;</span><br><span class="line">            type: <span class="string">"GET_DATA"</span>,</span><br><span class="line">            data: res.data</span><br><span class="line">        &#125;)</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>并不是所有的项目都需要用到Redux，同时在真实项目中，不建议所有的state都集中在Redux中处理，一般情况下只要把异步请求的数据和多个组件之间需要公用的数据，在Redux中集中管理即可。</p>

      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/04/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%E4%B8%8E%E7%BC%96%E7%A8%8B%E6%8A%80%E5%B7%A7/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/04/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%E4%B8%8E%E7%BC%96%E7%A8%8B%E6%8A%80%E5%B7%A7/" class="post-title-link" itemprop="url">设计原则与编程技巧</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              
    
              <time title="创建时间：2021-11-04 20:02:33 / 修改时间：21:39:14" itemprop="dateCreated datePublished" datetime="2021-11-04T20:02:33+08:00">2021-11-04</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/" itemprop="url" rel="index"><span itemprop="name">设计模式</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>本博客参考曾探所著&lt;&lt;JavaScript设计模式与开发实践&gt;&gt;，仅为个人学习总结，侵删。</p>
<h1 id="设计原则"><a href="#设计原则" class="headerlink" title="设计原则"></a>设计原则</h1><h2 id="单一职责原则"><a href="#单一职责原则" class="headerlink" title="单一职责原则"></a>单一职责原则</h2><p>SRP 原则体现为：一个对象（方法）只做一件事情</p>
<p>SRP 原则在很多设计模式中都有着广泛的运用，例如代理模式、迭代器模式、单例模式和装 饰者模式</p>
<p>SRP 原则是所有原则中最简单也是最难正确运用的原则之一。 要明确的是，并不是所有的职责都应该一一分离。 </p>
<p>一方面，如果随着需求的变化，有两个职责总是同时变化，那就不必分离他们。比如在 ajax 请求的时候，创建 xhr 对象和发送 xhr 请求几乎总是在一起的，那么创建 xhr 对象的职责和发送 xhr 请求的职责就没有必要分开。 </p>
<p>另一方面，职责的变化轴线仅当它们确定会发生变化时才具有意义，即使两个职责已经被耦 合在一起，但它们还没有发生改变的征兆，那么也许没有必要主动分离它们，在代码需要重构的 时候再进行分离也不迟</p>
<p>SRP 原则的优点是降低了单个类或者对象的复杂度，按照职责把对象分解成更小的粒度， 这有助于代码的复用，也有利于进行单元测试。当一个职责需要变更的时候，不会影响到其他 的职责。 </p>
<p>但 SRP 原则也有一些缺点，最明显的是会增加编写代码的复杂度。当我们按照职责把对象 分解成更小的粒度之后，实际上也增大了这些对象之间相互联系的难度</p>
<h2 id="最少知识原则"><a href="#最少知识原则" class="headerlink" title="最少知识原则"></a>最少知识原则</h2><p>最少知识原则（LKP）说的是一个软件实体应当尽可能少地与其他实体发生相互作用。这 里的软件实体是一个广义的概念，不仅包括对象，还包括系统、类、模块、函数、变量等。即迪米特法则</p>
<p>单一职责原则指导我们把对象划分成较小的粒度，这可以提高对象的可复用性。但越来越 多的对象之间可能会产生错综复杂的联系，如果修改了其中一个对象，很可能会影响到跟它相 互引用的其他对象。对象和对象耦合在一起，有可能会降低它们的可复用性。</p>
<p>最少知识原则要求我们在设计程序时，应当尽量减少对象之间的交互。如果两个对象之间不 必彼此直接通信，那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对 象，来承担这些对象之间的通信作用。如果一些对象需要向另一些对象发起请求，可以通过第三 者对象来转发这些请求。</p>
<p>最少知识原则在设计模式中体现得最多的地方是中介者模式和外观模式，</p>
<h2 id="开放封闭原则"><a href="#开放封闭原则" class="headerlink" title="开放封闭原则"></a>开放封闭原则</h2><p>在面向对象的程序设计中，开放封闭原则（OCP）是最重要的一条原则。很多时候，一个 程序具有良好的设计，往往说明它是符合开放封闭原则的。软件实体（类、模块、函数）等应该是可以扩展的，但是不可修改。</p>
<p>当需要改变一个程序的功能或者给这个程序增加新功 能的时候，可以使用增加代码的方式，但是不允许改动程序的源代码。</p>
<p>过多的条件分支语句是造成程序违反开放封闭原则的一个常见原因。每当需要增加一个新 的 if 语句时，都要被迫改动原函数。把 if 换成 switch-case 是没有用的，这是一种换汤不换药 的做法。实际上，每当我们看到一大片的 if 或者 swtich-case 语句时，第一时间就应该考虑，能 否利用对象的多态性来重构它们。 利用对象的多态性来让程序遵守开放封闭原则，是一个常用的技巧。</p>
<p>开放封闭原则是一个看起来比较虚幻的原则，并没有实际的模板教导我们怎样亦步亦趋地 实现它。但我们还是能找到一些让程序尽量遵守开放封闭原则的规律，最明显的就是找出程序 中将要发生变化的地方，然后把变化封装起来。</p>
<p>通过封装变化的方式，可以把系统中稳定不变的部分和容易变化的部分隔离开来。在系统的 演变过程中，我们只需要替换那些容易变化的部分，如果这些部分是已经被封装好的，那么替换 起来也相对容易。而变化部分之外的就是稳定的部分。在系统的演变过程中，稳定的部分是不需 要改变的。</p>
<p>除了利用对象的多态性之外，还有其他方式可以帮助我们编写遵守开放封闭原则的代码， 下面将详细介绍。 </p>
<p>1、放置挂钩：放置挂钩（hook）也是分离变化的一种方式。我们在程序有可能发生变化的地方放置一个挂 钩，挂钩的返回结果决定了程序的下一步走向。这样一来，原本的代码执行路径上就出现了一个 分叉路口，程序未来的执行方向被预埋下多种可能性。</p>
<p>2、使用回调函数：在 JavaScript 中，函数可以作为参数传递给另外一个函数，这是高阶函数的意义之一。在这 种情况下，我们通常会把这个函数称为回调函数。在 JavaScript 版本的设计模式中，策略模式和 命令模式等都可以用回调函数轻松实现。  回调函数是一种特殊的挂钩。我们可以把一部分易于变化的逻辑封装在回调函数里，然后把 回调函数当作参数传入一个稳定和封闭的函数中。当回调函数被执行的时候，程序就可以因为回 调函数的内部逻辑不同，而产生不同的结果。</p>
<h1 id="接口编程"><a href="#接口编程" class="headerlink" title="接口编程"></a>接口编程</h1><p>当我们谈到接口的时候，通常会涉及以下几种含义，下面先简单介绍。 我们经常说一个库或者模块对外提供了某某 API 接口。通过主动暴露的接口来通信，可以隐 藏软件系统内部的工作细节。这也是我们最熟悉的第一种接口含义。 </p>
<p>第二种接口是一些语言提供的关键字，比如 Java 的 interface。interface 关键字可以产生一 个完全抽象的类。这个完全抽象的类用来表示一种契约，专门负责建立类与类之间的联系。 </p>
<p>第三种接口即是我们谈论的“面向接口编程”中的接口，接口的含义在这里体现得更为抽象。 用《设计模式》中的话说就是： 接口是对象能响应的请求的集合</p>
<h2 id="Java中的接口"><a href="#Java中的接口" class="headerlink" title="Java中的接口"></a>Java中的接口</h2><p>总而言之，不关注对象的具体类型，而仅仅针对超类型中的“契约方法”来编写程序，可以 产生可靠性高的程序，也可以极大地减少子系统实现之间的相互依赖关系，这就是我们本章要讨 论的主题：  面向接口编程，而不是面向实现编程。 </p>
<p>奇怪的是，本节我们一直讨论的是抽象类，跟接口又有什么关系呢？实际上这里的接口并不 是指 interface，而是一个抽象的概念。 从过程上来看，“面向接口编程”其实是“面向超类型编程”。当对象的具体类型被隐藏在超 类型身后时，这些对象就可以相互替换使用，我们的关注点才能从对象的类型上转移到对象的行 为上。“面向接口编程”也可以看成面向抽象编程，即针对超类型中的 abstract 方法编程，接口 在这里被当成 abstract 方法中约定的契约行为。这些契约行为暴露了一个类或者对象能够做什 么，但是不关心具体如何去做</p>
<p>除了用抽象类来完成面向接口编程之外，使用 interface 也可以达到同样的效果。虽然很多 人在实际使用中刻意区分抽象类和 interface，但使用 interface 实际上也是继承的一种方式，叫 作接口继承。interface 同样可以用于向上转型，这也是让对象表现出多态性的一条途径，实现了同一个接 口的两个类就可以被相互替换使用</p>
<h2 id="JS中的接口"><a href="#JS中的接口" class="headerlink" title="JS中的接口"></a>JS中的接口</h2><p>通过前面的讲解，我们明白了抽象类和 interface 的作用主要都是以下两点。  通过向上转型来隐藏对象的真正类型，以表现对象的多态性。  约定类与类之间的一些契约行为。 对于 JavaScript 而言，因为 JavaScript 是一门动态类型语言，类型本身在 JavaScript 中是一个 相对模糊的概念。也就是说，不需要利用抽象类或者 interface 给对象进行“向上转型”。</p>
<p>除了 number、string、boolean 等基本数据类型之外，其他的对象都可以被看成“天生”被“向上转型”成了 Object 类型：</p>
<p>因为不需要进行向上转型，接口在 JavaScript 中的最大作用就退化到了检查代码的规范性。 比如检查某个对象是否实现了某个方法，或者检查是否给函数传入了预期类型的参数。如果忽略 了这两点，有可能会在代码中留下一些隐藏的 bug。比如我们尝试执行 obj 对象的 show 方法，但 是 obj 对象本身却没有实现这个方法，</p>
<p>作为一门解释执行的动态类型语言，把希 望寄托在编译器上是不可能了。如果要处理这类异常情况，我们只有手动编写一些接口检查的代码。</p>
<h3 id="TS编写"><a href="#TS编写" class="headerlink" title="TS编写"></a>TS编写</h3><p>TypeScript 是微软开发的一种编程语言，是 JavaScript 的一个超集。跟 CoffeeScript 类似， TypeScript 代码最终会被编译成原生的 JavaScript 代码执行。通过 TypeScript，我们可以使用静态 语言的方式来编写 JavaScript 程序。用 TypeScript 来实现一些设计模式，显得更加原汁原味。 TypeScript 目前的版本还没有提供对抽象类的支持，但是提供了 interface。</p>
<h1 id="代码重构"><a href="#代码重构" class="headerlink" title="代码重构"></a>代码重构</h1><p>1、提炼函数</p>
<p>在 JavaScript 开发中，我们大部分时间都在与函数打交道，所以我们希望这些函数有着良好 的命名，函数体内包含的逻辑清晰明了。如果一个函数过长，不得不加上若干注释才能让这个函 数显得易读一些，那这些函数就很有必要进行重构。 如果在函数中有一段代码可以被独立出来，那我们最好把这些代码放进另外一个独立的函数 中。这是一种很常见的优化工作，这样做的好处主要有以下几点。 </p>
<p>避免出现超大函数。独立出来的函数有助于代码复用。独立出来的函数更容易被覆写。</p>
<p>2、合并重复的条件片段</p>
<p>如果一个函数体内有一些条件分支语句，而这些条件分支语句内部散布了一些重复的代码， 那么就有必要进行合并去重工作。</p>
<p>3、把条件分支语句提炼成函数</p>
<p>在程序设计中，复杂的条件分支语句是导致程序难以阅读和理解的重要原因，而且容易导致 一个庞大的函数。</p>
<p>4、合理使用循环</p>
<p>在函数体内，如果有些代码实际上负责的是一些重复性的工作，那么合理利用循环不仅可以 完成同样的功能，还可以使代码量更少。</p>
<p>5、提前让函数退出代替嵌套条件分支</p>
<p>在函数体内，如果有些代码实际上负责的是一些重复性的工作，那么合理利用循环不仅可以 完成同样的功能，还可以使代码量更少。</p>
<p>6、传递对象参数代替过长的参数列表</p>
<p>有时候一个函数有可能接收多个参数，而参数的数量越多，函数就越难理解和使用。</p>
<p>7、尽量减少参数数量</p>
<p>如果调用一个函数时需要传入多个参数，那这个函数是让人望而生畏的，我们必须搞清楚这 些参数代表的含义，必须小心翼翼地把它们按照顺序传入该函数。而如果一个函数不需要传入任 何参数就可以使用，这种函数是深受人们喜爱的。在实际开发中，向函数传递参数不可避免，但 我们应该尽量减少函数接收的参数数量。</p>
<p>8、少用三目运算符</p>
<p>9、合理使用链式调用</p>
<p>10、分解大型类</p>
<p>11、用 return 退出多重循环 </p>
<p>假设在函数体内有一个两重循环语句，我们需要在内层循环中判断，当达到某个临界条件时 退出外层的循环。我们大多数时候会引入一个控制标记变量：</p>

      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/04/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/JavaScript%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E5%9B%9B%EF%BC%89/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/04/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/JavaScript%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E5%9B%9B%EF%BC%89/" class="post-title-link" itemprop="url">JavaScript的设计模式（三）</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              
    
              <time title="创建时间：2021-11-04 20:02:33 / 修改时间：21:15:52" itemprop="dateCreated datePublished" datetime="2021-11-04T20:02:33+08:00">2021-11-04</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/" itemprop="url" rel="index"><span itemprop="name">设计模式</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>本博客参考曾探所著&lt;&lt;JavaScript设计模式与开发实践&gt;&gt;，仅为个人学习总结，侵删。</p>
<h1 id="装饰器模式"><a href="#装饰器模式" class="headerlink" title="装饰器模式"></a>装饰器模式</h1><p>在程序开发中，许多时候都并不希望某个类天 生就非常庞大，一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地 给某个对象添加一些额外的职责，而不会影响从这个类中派生的其他对象。 </p>
<p>在传统的面向对象语言中，给对象添加功能常常使用继承的方式，但是继承的方式并不灵活， 还会带来许多问题：一方面会导致超类和子类之间存在强耦合性，当超类改变时，子类也会随之 改变；另一方面，继承这种功能复用方式通常被称为“白箱复用”，“白箱”是相对可见性而言的， 在继承方式中，超类的内部细节是对子类可见的，继承常常被认为破坏了封装性。  使用继承还会带来另外一个问题，在完成一些功能复用的同时，有可能创建出大量的子类， 使子类的数量呈爆炸性增长。</p>
<p>这种给对象动态地增加职责的方式称为装 饰者（decorator）模式。装饰者模式能够在不改 变对象自身的基础上，在程序运行期间给对象 动态地添加职责。跟继承相比，装饰者是一种 更轻便灵活的做法，这是一种“即用即付”的 方式，</p>
<p>首先要提出来的是，作为一门解释执行的语言，给 JavaScript 中的对象动态添加或者改变职 责是一件再简单不过的事情，虽然这种做法改动了对象自身，跟传统定义中的装饰者模式并不一 样，但这无疑更符合 JavaScript 的语言特色。代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> obj = &#123; </span><br><span class="line">    name: <span class="string">'sven'</span>, </span><br><span class="line">    address: <span class="string">'深圳市'</span> </span><br><span class="line"> &#125;; </span><br><span class="line">obj.address = obj.address + <span class="string">'福田区'</span>;</span><br></pre></td></tr></table></figure>

<p>传统面向对象语言中的装饰者模式在 JavaScript 中适用的场景并不多，如上面代码所示，通 常我们并不太介意改动对象自身。尽管如此，本节我们还是稍微模拟一下传统面向对象语言中的 装饰者模式实现。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//下面来看代码实现，首先是原始的飞机类：</span></span><br><span class="line"><span class="keyword">var</span> Plane = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125; </span><br><span class="line">Plane.prototype.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射普通子弹'</span> ); </span><br><span class="line">&#125; </span><br><span class="line"><span class="comment">//接下来增加两个装饰类，分别是导弹和原子弹：</span></span><br><span class="line"><span class="keyword">var</span> MissileDecorator = <span class="function"><span class="keyword">function</span>(<span class="params"> plane </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plane = plane; </span><br><span class="line">&#125; </span><br><span class="line">MissileDecorator.prototype.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plane.fire(); </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射导弹'</span> ); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> AtomDecorator = <span class="function"><span class="keyword">function</span>(<span class="params"> plane </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plane = plane; </span><br><span class="line">&#125; </span><br><span class="line">AtomDecorator.prototype.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plane.fire(); </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射原子弹'</span> ); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>导弹类和原子弹类的构造函数都接受参数 plane 对象，并且保存好这个参数，在它们的 fire 方法中，除了执行自身的操作之外，还调用 plane 对象的 fire 方法。 </p>
<p>这种给对象动态增加职责的方式，并没有真正地改动对象自身，而是将对象放入另一个对象 之中，这些对象以一条链的方式进行引用，形成一个聚合对象。这些对象都拥有相同的接口（fire 方法），当请求达到链中的某个对象时，这个对象会执行自身的操作，随后把请求转发给链中的 下一个对象。 因为装饰者对象和它所装饰的对象拥有一致的接口，所以它们对使用该对象的客户来说是透 明的，被装饰的对象也并不需要了解它曾经被装饰过，这种透明性使得我们可以递归地嵌套任意 多个装饰者对象，</p>
<p>装饰器其实也是包装器：从功能上而言，decorator 能很好地描述这 个模式，但从结构上看，wrapper 的说法更加 贴切。装饰者模式将一个对象嵌入另一个对象 之中，实际上相当于这个对象被另一个对象包 装起来，形成一条包装链。请求随着这条链依 次传递到所有的对象，每个对象都有处理这条 请求的机会。</p>
<h2 id="装饰函数"><a href="#装饰函数" class="headerlink" title="装饰函数"></a>装饰函数</h2><p>JavaScript 语言动态改变对象相当容易，我们可以直接改写对象或者对象的某个方法，并不 需要使用“类”来实现装饰者模式，代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> plane = &#123; </span><br><span class="line">    fire: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'发射普通子弹'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> missileDecorator = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射导弹'</span> ); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> atomDecorator = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射原子弹'</span> ); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> fire1 = plane.fire; </span><br><span class="line">plane.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    fire1(); </span><br><span class="line">    missileDecorator(); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> fire2 = plane.fire; </span><br><span class="line">plane.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    fire2(); </span><br><span class="line">    atomDecorator(); </span><br><span class="line">&#125; </span><br><span class="line">plane.fire(); </span><br><span class="line"><span class="comment">// 分别输出： 发射普通子弹、发射导弹、发射原子弹</span></span><br></pre></td></tr></table></figure>

<p>在 JavaScript 中，几乎一切都是对象，其中函数又被称为一等对象。在平时的开发工作中， 也许大部分时间都在和函数打交道。在 JavaScript 中可以很方便地给某个对象扩展属性和方法， 但却很难在不改动某个函数源代码的情况下，给该函数添加一些额外的功能。</p>
<p>在代码的运行期间， 我们很难切入某个函数的执行环境。 要想为函数添加一些功能，最简单粗暴的方式就是直接改写该函数，但这是最差的办法，直 接违反了开放-封闭原则</p>
<p>很多时候我们不想去碰原函数，也许原函数是由其他同事编写的，里面的实现非常杂乱。甚 至在一个古老的项目中，这个函数的源代码被隐藏在一个我们不愿碰触的阴暗角落里。现在需要 一个办法，在不改变函数源代码的情况下，能给函数增加功能，这正是开放-封闭原则给我们指出的光明道路。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//其实在 15.3 节的代码中，我们已经找到了一种答案，通过保存原引用的方式就可以改写某个函数：</span></span><br><span class="line"><span class="keyword">var</span> a = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    alert (<span class="number">1</span>); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> _a = a; </span><br><span class="line">a = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    _a(); </span><br><span class="line">    alert (<span class="number">2</span>); </span><br><span class="line">&#125; </span><br><span class="line">a(); </span><br><span class="line"><span class="comment">/*这是实际开发中很常见的一种做法，比如我们想给 window 绑定 onload 事件，但是又不确定</span></span><br><span class="line"><span class="comment">这个事件是不是已经被其他人绑定过，为了避免覆盖掉之前的 window.onload 函数中的行为，我</span></span><br><span class="line"><span class="comment">们一般都会先保存好原先的 window.onload，把它放入新的 window.onload 里执行：*/</span></span><br><span class="line"><span class="built_in">window</span>.onload = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    alert (<span class="number">1</span>); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> _onload = <span class="built_in">window</span>.onload || <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125;; </span><br><span class="line"><span class="built_in">window</span>.onload = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    _onload(); </span><br><span class="line">    alert (<span class="number">2</span>); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这样的代码当然是符合开放封闭原则的，我们在增加新功能的时候，确实没有修改原来的 window.onload 代码，但是这种方式存在以下两个问题。</p>
<p>1、必须维护_onload 这个中间变量，虽然看起来并不起眼，但如果函数的装饰链较长，或者 需要装饰的函数变多，这些中间变量的数量也会越来越多。</p>
<p>2、 其实还遇到了 this 被劫持的问题，在 window.onload 的例子中没有这个烦恼，是因为调用 普通函数_onload 时，this 也指向 window，跟调用 window.onload 时一样（函数作为对象的 方法被调用时，this 指向该对象，所以此处 this 也只指向 window）。现在把 window.onload 换成 document.getElementById，</p>
<h2 id="AOP装饰函数"><a href="#AOP装饰函数" class="headerlink" title="AOP装饰函数"></a>AOP装饰函数</h2><p>首先给出Function.prototype.before 方法和 Function.prototype.after 方法：这种AOP编程的方式和洋葱模型的before、after插件执行方式类似：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Function</span>.prototype.before = <span class="function"><span class="keyword">function</span>(<span class="params"> beforefn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> __self = <span class="keyword">this</span>; <span class="comment">// 保存原函数的引用</span></span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; <span class="comment">// 返回包含了原函数和新函数的"代理"函数</span></span><br><span class="line">        beforefn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); <span class="comment">// 执行新函数，且保证 this 不被劫持，新函数接受的参数</span></span><br><span class="line">        <span class="comment">// 也会被原封不动地传入原函数，新函数在原函数之前执行</span></span><br><span class="line">        <span class="keyword">return</span> __self.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); <span class="comment">// 执行原函数并返回原函数的执行结果，</span></span><br><span class="line"> <span class="comment">// 并且保证 this 不被劫持</span></span><br><span class="line">    &#125; </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="built_in">Function</span>.prototype.after = <span class="function"><span class="keyword">function</span>(<span class="params"> afterfn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> __self = <span class="keyword">this</span>; </span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">var</span> ret = __self.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); </span><br><span class="line">        afterfn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> );</span><br><span class="line">        <span class="keyword">return</span> ret; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>Function.prototype.before 接受一个函数当作参数，这个函数即为新添加的函数，它装载了 新添加的功能代码。 接下来把当前的 this 保存起来，这个 this 指向原函数，然后返回一个“代理”函数，这个 “代理”函数只是结构上像代理而已，并不承担代理的职责（比如控制对象的访问等）。它的工作 是把请求分别转发给新添加的函数和原函数，且负责保证它们的执行顺序，让新添加的函数在原 函数之前执行（前置装饰），这样就实现了动态装饰的效果。 我们注意到，通过 Function.prototype.apply 来动态传入正确的 this，保证了函数在被装饰 之后，this 不会被劫持。 Function.prototype.after 的原理跟 Function.prototype.before 一模一样，唯一不同的地方在 于让新添加的函数在原函数执行之后再执行。</p>
<p>值得提到的是，上面的 AOP 实现是在 Function.prototype 上添加 before 和 after 方法，但许 多人不喜欢这种污染原型的方式，那么我们可以做一些变通，把原函数和新函数都作为参数传入 before 或者 after 方法：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> before = <span class="function"><span class="keyword">function</span>(<span class="params"> fn, beforefn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        beforefn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); </span><br><span class="line">        <span class="keyword">return</span> fn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> a = before( </span><br><span class="line">    <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;alert (<span class="number">3</span>)&#125;, </span><br><span class="line">    <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;alert (<span class="number">2</span>)&#125; </span><br><span class="line">); </span><br><span class="line">a = before( a, <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;alert (<span class="number">1</span>);&#125; ); </span><br><span class="line">a();</span><br></pre></td></tr></table></figure>

<p>用 AOP 装饰函数的技巧在实际开发中非常有用。不论是业务代码的编写，还是在框架层面， 我们都可以把行为依照职责分成粒度更细的函数，随后通过装饰把它们合并到一起，这有助于我 们编写一个松耦合和高复用性的系统。</p>
<h3 id="动态修改参数"><a href="#动态修改参数" class="headerlink" title="动态修改参数"></a>动态修改参数</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Function</span>.prototype.before = <span class="function"><span class="keyword">function</span>(<span class="params"> beforefn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> __self = <span class="keyword">this</span>; </span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        beforefn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); <span class="comment">// (1) </span></span><br><span class="line">        <span class="keyword">return</span> __self.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); <span class="comment">// (2) </span></span><br><span class="line">    &#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>从这段代码的(1)处 和(2)处可以看到，beforefn 和原函数__self 共用一组参数列表 arguments，当我们在 beforefn 的函数体内改变 arguments 的时候，原函数__self 接收的参数列 表自然也会变化。 下面的例子展示了如何通过 Function.prototype.before 方法给函数 func 的参数 param 动态地 添加属性 b：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> func = <span class="function"><span class="keyword">function</span>(<span class="params"> param </span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( param ); <span class="comment">// 输出： &#123;a: "a", b: "b"&#125; </span></span><br><span class="line">&#125; </span><br><span class="line">func = func.before( <span class="function"><span class="keyword">function</span>(<span class="params"> param </span>)</span>&#123; </span><br><span class="line">    param.b = <span class="string">'b'</span>; </span><br><span class="line">&#125;); </span><br><span class="line">func( &#123;<span class="attr">a</span>: <span class="string">'a'</span>&#125; );</span><br></pre></td></tr></table></figure>

<h3 id="插件式表单验证"><a href="#插件式表单验证" class="headerlink" title="插件式表单验证"></a>插件式表单验证</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Function</span>.prototype.before = <span class="function"><span class="keyword">function</span>(<span class="params"> beforefn </span>)</span>&#123; </span><br><span class="line"> <span class="keyword">var</span> __self = <span class="keyword">this</span>; </span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line"> <span class="keyword">if</span> ( beforefn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ) === <span class="literal">false</span> )&#123; </span><br><span class="line"> <span class="comment">// beforefn 返回 false 的情况直接 return，不再执行后面的原函数</span></span><br><span class="line"> <span class="keyword">return</span>; </span><br><span class="line"> &#125; </span><br><span class="line"> <span class="keyword">return</span> __self.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); </span><br><span class="line"> &#125; </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> validata = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line"> <span class="keyword">if</span> ( username.value === <span class="string">''</span> )&#123; </span><br><span class="line"> alert ( <span class="string">'用户名不能为空'</span> ); </span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>; </span><br><span class="line"> &#125; </span><br><span class="line"> <span class="keyword">if</span> ( password.value === <span class="string">''</span> )&#123; </span><br><span class="line"> alert ( <span class="string">'密码不能为空'</span> ); </span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>; </span><br><span class="line"> &#125; </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> formSubmit = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line"> <span class="keyword">var</span> param = &#123; </span><br><span class="line"> username: username.value, </span><br><span class="line"> password: password.value </span><br><span class="line"> &#125; </span><br><span class="line"> ajax( <span class="string">'http:// xxx.com/login'</span>, param ); </span><br><span class="line">&#125; </span><br><span class="line">formSubmit = formSubmit.before( validata ); </span><br><span class="line">submitBtn.onclick = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line"> formSubmit(); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在这段代码中，校验输入和提交表单的代码完全分离开来，它们不再有任何耦合关系， formSubmit = formSubmit.before( validata )这句代码，如同把校验规则动态接在 formSubmit 函数 之前，validata 成为一个即插即用的函数，它甚至可以被写成配置文件的形式，这有利于我们分 开维护这两个函数。再利用策略模式稍加改造，我们就可以把这些校验规则都写成插件的形式， 用在不同的项目当中。</p>
<p>值得注意的是，因为函数通过 Function.prototype.before 或者 Function.prototype.after 被装 饰之后，返回的实际上是一个新的函数，如果在原函数上保存了一些属性，那么这些属性会丢失。</p>
<p>另外，这种装饰方式也叠加了函数的作用域，如果装饰的链条过长，性能上也会受到一些 影响。</p>
<h3 id="装饰与代理"><a href="#装饰与代理" class="headerlink" title="装饰与代理"></a>装饰与代理</h3><p>装饰者模式和第 6 章代理模式的结构看起来非常相像，这两种模式都描述了怎样为对象提供 一定程度上的间接引用，它们的实现部分都保留了对另外一个对象的引用，并且向那个对象发送 请求。 </p>
<p>代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是，当直 接访问本体不方便或者不符合需要时，为这个本体提供一个替代者。本体定义了关键功能，而代 理提供或拒绝对它的访问，或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对 象动态加入行为。换句话说，代理模式强调一种关系（Proxy 与它的实体之间的关系），这种关系 可以静态的表达，也就是说，这种关系在一开始就可以被确定。</p>
<p>而装饰者模式用于一开始不能确 定对象的全部功能时。代理模式通常只有一层代理本体的引用，而装饰者模式经常会形成一条 长长的装饰链。 在虚拟代理实现图片预加载的例子中，本体负责设置 img 节点的 src，代理则提供了预加载 的功能，这看起来也是“加入行为”的一种方式，但这种加入行为的方式和装饰者模式的偏重点 是不一样的。装饰者模式是实实在在的为对象增加新的职责和行为，而代理做的事情还是跟本体 一样，最终都是设置 src。但代理可以加入一些“聪明”的功能，比如在图片真正加载好之前，先使用一张占位的 loading 图片反馈给客户</p>
<p>装饰函数，它是 JavaScript 中独特的装饰者模式。这种模式在实际开发中非常 有用，除了上面提到的例子，它在框架开发中也十分有用。作为框架作者，我们希望框架里的函 数提供的是一些稳定而方便移植的功能，那些个性化的功能可以在框架之外动态装饰上去，这可 以避免为了让框架拥有更多的功能，而去使用一些 if、else 语句预测用户的实际需要。</p>
<h1 id="状态模式"><a href="#状态模式" class="headerlink" title="状态模式"></a>状态模式</h1><p>状态模式是一种非同寻常的优秀模式，它也许是解决某些需求场景的最好方法。虽然状态模 式并不是一种简单到一目了然的模式（它往往还会带来代码量的增加），但你一旦明白了状态模 式的精髓，以后一定会感谢它带给你的无与伦比的好处。 状态模式的关键是区分事物内部的状态，事物内部状态的改变往往会带来事物的行为改变。 </p>
<p>状态模式和策略模式像一对双胞胎，它们都封装了一系列的算法或者行为，它们的类图看起来几乎一模一样，但在意图上有很大不同，因此它们是两种迥然不同的模式。 </p>
<p>策略模式和状态模式的相同点是，它们都有一个上下文、一些策略或者状态类，上下文把请 求委托给这些类来执行。 它们之间的区别是策略模式中的各个策略类之间是平等又平行的，它们之间没有任何联系， 所以客户必须熟知这些策略类的作用，以便客户可以随时主动切换算法；而在状态模式中，状态 和状态对应的行为是早已被封装好的，状态之间的切换也早被规定完成，“改变行为”这件事情 发生在状态模式内部。对客户来说，并不需要了解这些细节。这正是状态模式的作用所在。</p>
<h2 id="电灯程序"><a href="#电灯程序" class="headerlink" title="电灯程序"></a>电灯程序</h2><p>现在我们学习使用状态模式改进电灯的程序。有意思的是，通常我们谈到封装，一般都会优 先封装对象的行为，而不是对象的状态。但在状态模式中刚好相反，状态模式的关键是把事物的 每种状态都封装成单独的类，跟此种状态有关的行为都被封装在这个类的内部，所以 button 被按 下的的时候，只需要在上下文中，把这个请求委托给当前的状态对象即可，该状态对象会负责渲 染它自身的行为，</p>
<p>同时我们还可以把状态的切换规则事先分布在状态类中， 这样就有效地消除了原本存在的 大量条件分支语句，下面进入状态模式的代码编写阶段，首先将定义 3 个状态类，分别是 offLightState、 WeakLightState、strongLightState。这 3 个类都有一个原型方法 buttonWasPressed，代表在各自状态下，按钮被按下时将发生的行为，代码如下</p>
<p><img src="/../../image/JavaScript%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E5%9B%9B%EF%BC%89/image-20211104193809528.png" alt="image-20211104193809528"></p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// OffLightState：</span></span><br><span class="line"><span class="keyword">var</span> OffLightState = <span class="function"><span class="keyword">function</span>(<span class="params"> light </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.light = light; </span><br><span class="line">&#125;; </span><br><span class="line">OffLightState.prototype.buttonWasPressed = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'弱光'</span> ); <span class="comment">// offLightState 对应的行为</span></span><br><span class="line">    <span class="keyword">this</span>.light.setState( <span class="keyword">this</span>.light.weakLightState ); <span class="comment">// 切换状态到 weakLightState </span></span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">// WeakLightState：</span></span><br><span class="line"><span class="keyword">var</span> WeakLightState = <span class="function"><span class="keyword">function</span>(<span class="params"> light </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.light = light; </span><br><span class="line">&#125;; </span><br><span class="line">WeakLightState.prototype.buttonWasPressed = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'强光'</span> ); <span class="comment">// weakLightState 对应的行为</span></span><br><span class="line">    <span class="keyword">this</span>.light.setState( <span class="keyword">this</span>.light.strongLightState ); <span class="comment">// 切换状态到 strongLightState </span></span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">// StrongLightState：</span></span><br><span class="line"><span class="keyword">var</span> StrongLightState = <span class="function"><span class="keyword">function</span>(<span class="params"> light </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.light = light; </span><br><span class="line">&#125;; </span><br><span class="line">StrongLightState.prototype.buttonWasPressed = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'关灯'</span> ); <span class="comment">// strongLightState 对应的行为</span></span><br><span class="line">    <span class="keyword">this</span>.light.setState( <span class="keyword">this</span>.light.offLightState ); <span class="comment">// 切换状态到 offLightState </span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>接下来改写 Light 类，现在不再使用一个字符串来记录当前的状态，而是使用更加立体化的 状态对象。我们在 Light 类的构造函数里为每个状态类都创建一个状态对象，这样一来我们可以 很明显地看到电灯一共有多少种状态，代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Light = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.offLightState = <span class="keyword">new</span> OffLightState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.weakLightState = <span class="keyword">new</span> WeakLightState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.strongLightState = <span class="keyword">new</span> StrongLightState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.button = <span class="literal">null</span>; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">在 button 按钮被按下的事件里，Context 也不再直接进行任何实质性的操作，而是通过</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">Light.prototype.init = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> button = <span class="built_in">document</span>.createElement( <span class="string">'button'</span> ), </span><br><span class="line">        self = <span class="keyword">this</span>;</span><br><span class="line">    <span class="keyword">this</span>.button = <span class="built_in">document</span>.body.appendChild( button ); </span><br><span class="line">    <span class="keyword">this</span>.button.innerHTML = <span class="string">'开关'</span>; </span><br><span class="line">    <span class="keyword">this</span>.currState = <span class="keyword">this</span>.offLightState; <span class="comment">// 设置当前状态</span></span><br><span class="line">    <span class="keyword">this</span>.button.onclick = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        self.currState.buttonWasPressed(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">最后还要提供一个 Light.prototype.setState 方法，状态对象可以通过这个方法来切换 light</span></span><br><span class="line"><span class="comment">对象的状态。前面已经说过，状态的切换规律事先被完好定义在各个状态类中。在 Context 中再</span></span><br><span class="line"><span class="comment">也找不到任何一个跟状态切换相关的条件分支语句：</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">Light.prototype.setState = <span class="function"><span class="keyword">function</span>(<span class="params"> newState </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.currState = newState; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> light = <span class="keyword">new</span> Light(); </span><br><span class="line">light.init(); </span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">状态模式的好处很明显，它可以使每</span></span><br><span class="line"><span class="comment">一种状态和它对应的行为之间的关系局部化，这些行为被分散和封装在各自对应的状态类之中，</span></span><br><span class="line"><span class="comment">便于阅读和管理代码。</span></span><br><span class="line"><span class="comment">另外，状态之间的切换都被分布在状态类内部，这使得我们无需编写过多的 if、else 条件分支语言来控制状态之间的转换。</span></span><br><span class="line"><span class="comment">当我们需要为 light 对象增加一种新的状态时，只需要增加一个新的状态类，再稍稍改变一</span></span><br><span class="line"><span class="comment">些现有的代码即可。假设现在 light 对象多了一种超强光的状态，那就先增加 SuperStrongLightState类</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">var</span> SuperStrongLightState = <span class="function"><span class="keyword">function</span>(<span class="params"> light </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.light = light; </span><br><span class="line">&#125;; </span><br><span class="line">SuperStrongLightState.prototype.buttonWasPressed = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'关灯'</span> ); </span><br><span class="line">    <span class="keyword">this</span>.light.setState( <span class="keyword">this</span>.light.offLightState ); </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">//然后在 Light 构造函数里新增一个 superStrongLightState 对象：</span></span><br><span class="line"><span class="keyword">var</span> Light = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.offLightState = <span class="keyword">new</span> OffLightState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.weakLightState = <span class="keyword">new</span> WeakLightState( <span class="keyword">this</span> );</span><br><span class="line">    <span class="keyword">this</span>.strongLightState = <span class="keyword">new</span> StrongLightState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.superStrongLightState = <span class="keyword">new</span> SuperStrongLightState( <span class="keyword">this</span> ); <span class="comment">// 新增 superStrongLightState 对象</span></span><br><span class="line">    <span class="keyword">this</span>.button = <span class="literal">null</span>; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">/*最后改变状态类之间的切换规则，从 StrongLightState----&gt;OffLightState 变为 StrongLightState----&gt;SuperStrongLightState ----&gt;OffLightState：*/</span></span><br><span class="line">StrongLightState.prototype.buttonWasPressed = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'超强光'</span> ); <span class="comment">// strongLightState 对应的行为</span></span><br><span class="line">    <span class="keyword">this</span>.light.setState( <span class="keyword">this</span>.light.superStrongLightState ); <span class="comment">// 切换状态到 offLightState </span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h2 id="通用结构"><a href="#通用结构" class="headerlink" title="通用结构"></a>通用结构</h2><p>状态模式的定义： 允许一个对象在其内部状态改变时改变它的行为，对象看起来似乎修改了它的类。 我们以逗号分割，把这句话分为两部分来看。</p>
<p>第一部分的意思是将状态封装成独立的类，并 将请求委托给当前的状态对象，当对象的内部状态改变时，会带来不同的行为变化。电灯的例子 足以说明这一点，在 off 和 on 这两种不同的状态下，我们点击同一个按钮，得到的行为反馈是截 然不同的。 </p>
<p>第二部分是从客户的角度来看，我们使用的对象，在不同的状态下具有截然不同的行为，这个对象看起来是从不同的类中实例化而来的，实际上这是使用了委托的效果。</p>
<p>在前面的电灯例子中，我们完成了一个状态模式程序的编写。首先定义了 Light 类，Light 类在这里也被称为上下文（Context）。随后在 Light 的构造函数中，我们要创建每一个状态类的 实例对象，Context 将持有这些状态对象的引用，以便把请求委托给状态对象。用户的请求，即 点击 button 的动作也是实现在 Context 中的，代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Light = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.offLightState = <span class="keyword">new</span> OffLightState( <span class="keyword">this</span> ); <span class="comment">// 持有状态对象的引用</span></span><br><span class="line">    <span class="keyword">this</span>.weakLightState = <span class="keyword">new</span> WeakLightState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.strongLightState = <span class="keyword">new</span> StrongLightState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.superStrongLightState = <span class="keyword">new</span> SuperStrongLightState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.button = <span class="literal">null</span>; </span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">Light.prototype.init = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> button = <span class="built_in">document</span>.createElement( <span class="string">'button'</span> ), </span><br><span class="line">        self = <span class="keyword">this</span>; </span><br><span class="line">    <span class="keyword">this</span>.button = <span class="built_in">document</span>.body.appendChild( button ); </span><br><span class="line">    <span class="keyword">this</span>.button.innerHTML = <span class="string">'开关'</span>; </span><br><span class="line">    <span class="keyword">this</span>.currState = <span class="keyword">this</span>.offLightState; <span class="comment">// 设置默认初始状态</span></span><br><span class="line">    <span class="keyword">this</span>.button.onclick = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; <span class="comment">// 定义用户的请求动作</span></span><br><span class="line">        self.currState.buttonWasPressed(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">接下来可能是个苦力活，我们要编写各种状态类，light 对象被传入状态类的构造函数，状</span></span><br><span class="line"><span class="comment">态对象也需要持有 light 对象的引用，以便调用 light 中的方法或者直接操作 light 对象：</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">var</span> OffLightState = <span class="function"><span class="keyword">function</span>(<span class="params"> light </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.light = light; </span><br><span class="line">&#125;;</span><br><span class="line">OffLightState.prototype.buttonWasPressed = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'弱光'</span> ); </span><br><span class="line">    <span class="keyword">this</span>.light.setState( <span class="keyword">this</span>.light.weakLightState ); </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h3 id="缺少抽象类"><a href="#缺少抽象类" class="headerlink" title="缺少抽象类"></a>缺少抽象类</h3><p>我们看到，在状态类中将定义一些共同的行为方法，Context 最终会将请求委托给状态对象 的这些方法，在这个例子里，这个方法就是 buttonWasPressed。无论增加了多少种状态类，它们 都必须实现 buttonWasPressed 方法。</p>
<p>在 Java 中，所有的状态类必须继承自一个 State 抽象父类，当然如果没有共同的功能值得放 入抽象父类中，也可以选择实现 State 接口。这样做的原因一方面是我们曾多次提过的向上转型， 另一方面是保证所有的状态子类都实现了 buttonWasPressed 方法。遗憾的是，JavaScript 既不支持 抽象类，也没有接口的概念。所以在使用状态模式的时候要格外小心，如果我们编写一个状态子 类时，忘记了给这个状态子类实现 buttonWasPressed 方法，则会在状态切换的时候抛出异常。因 为 Context 总是把请求委托给状态对象的 buttonWasPressed 方法</p>
<p>不论怎样严格要求程序员，也许都避免不了犯错的那一天，毕竟如果没有编译器的帮助，只 依靠程序员的自觉以及一点好运气，是不靠谱的。这里建议的解决方案跟《模板方法模式》中一 致，让抽象父类的抽象方法直接抛出一个异常，这个异常至少会在程序运行期间就被发现：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> State = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125;; </span><br><span class="line">State.prototype.buttonWasPressed = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'父类的 buttonWasPressed 方法必须被重写'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> SuperStrongLightState = <span class="function"><span class="keyword">function</span>(<span class="params"> light </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.light = light; </span><br><span class="line">&#125;; </span><br><span class="line">SuperStrongLightState.prototype = <span class="keyword">new</span> State(); <span class="comment">// 继承抽象父类</span></span><br><span class="line">SuperStrongLightState.prototype.buttonWasPressed = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; <span class="comment">// 重写 buttonWasPressed 方法</span></span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'关灯'</span> ); </span><br><span class="line">    <span class="keyword">this</span>.light.setState( <span class="keyword">this</span>.light.offLightState ); </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h2 id="文件上传程序"><a href="#文件上传程序" class="headerlink" title="文件上传程序"></a>文件上传程序</h2><p>状态模式在文件上传的程序中，是最优雅的解决办法之一。通过电灯的例子，我们已经熟知 状态模式的结构了，下面就开始一步步地完成它。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//第一步仍然是提供 window.external.upload 函数，在页面中模拟创建上传插件，这部分代码 没有改变。</span></span><br><span class="line"><span class="built_in">window</span>.external.upload = <span class="function"><span class="keyword">function</span>(<span class="params"> state </span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( state ); <span class="comment">// 可能为 sign、uploading、done、error </span></span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> plugin = (<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> plugin = <span class="built_in">document</span>.createElement( <span class="string">'embed'</span> ); </span><br><span class="line">    plugin.style.display = <span class="string">'none'</span>; </span><br><span class="line">    plugin.type = <span class="string">'application/txftn-webkit'</span>; </span><br><span class="line">    plugin.sign = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'开始文件扫描'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">    plugin.pause = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'暂停文件上传'</span> ); </span><br><span class="line">    &#125;; </span><br><span class="line">    plugin.uploading = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'开始文件上传'</span> ); </span><br><span class="line">    &#125;; </span><br><span class="line">    plugin.del = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'删除文件上传'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">    plugin.done = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'文件上传完成'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="built_in">document</span>.body.appendChild( plugin ); </span><br><span class="line">    <span class="keyword">return</span> plugin; </span><br><span class="line">&#125;)(); </span><br><span class="line"></span><br><span class="line"><span class="comment">//第二步，改造 Upload 构造函数，在构造函数中为每种状态子类都创建一个实例对象：</span></span><br><span class="line"><span class="keyword">var</span> Upload = <span class="function"><span class="keyword">function</span>(<span class="params"> fileName </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plugin = plugin; </span><br><span class="line">    <span class="keyword">this</span>.fileName = fileName; </span><br><span class="line">    <span class="keyword">this</span>.button1 = <span class="literal">null</span>; </span><br><span class="line">    <span class="keyword">this</span>.button2 = <span class="literal">null</span>; </span><br><span class="line">    <span class="keyword">this</span>.signState = <span class="keyword">new</span> SignState( <span class="keyword">this</span> ); <span class="comment">// 设置初始状态为 waiting </span></span><br><span class="line">    <span class="keyword">this</span>.uploadingState = <span class="keyword">new</span> UploadingState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.pauseState = <span class="keyword">new</span> PauseState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.doneState = <span class="keyword">new</span> DoneState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.errorState = <span class="keyword">new</span> ErrorState( <span class="keyword">this</span> ); </span><br><span class="line">    <span class="keyword">this</span>.currState = <span class="keyword">this</span>.signState; <span class="comment">// 设置当前状态</span></span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">//第三步，Upload.prototype.init 方法无需改变，仍然负责往页面中创建跟上传流程有关的DOM 节点，并开始绑定按钮的事件</span></span><br><span class="line">Upload.prototype.init = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> that = <span class="keyword">this</span>; </span><br><span class="line">    <span class="keyword">this</span>.dom = <span class="built_in">document</span>.createElement( <span class="string">'div'</span> ); </span><br><span class="line">    <span class="keyword">this</span>.dom.innerHTML = </span><br><span class="line"> <span class="string">'&lt;span&gt;文件名称:'</span>+ <span class="keyword">this</span>.fileName +<span class="string">'&lt;/span&gt;\ </span></span><br><span class="line"><span class="string"> &lt;button data-action="button1"&gt;扫描中&lt;/button&gt;\ </span></span><br><span class="line"><span class="string"> &lt;button data-action="button2"&gt;删除&lt;/button&gt;'</span>; </span><br><span class="line">    <span class="built_in">document</span>.body.appendChild( <span class="keyword">this</span>.dom ); </span><br><span class="line">    <span class="keyword">this</span>.button1 = <span class="keyword">this</span>.dom.querySelector( <span class="string">'[data-action="button1"]'</span> ); </span><br><span class="line">    <span class="keyword">this</span>.button2 = <span class="keyword">this</span>.dom.querySelector( <span class="string">'[data-action="button2"]'</span> ); </span><br><span class="line">    <span class="keyword">this</span>.bindEvent(); </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">//第四步，负责具体的按钮事件实现，在点击了按钮之后，Context 并不做任何具体的操作，而是把请求委托给当前的状态类来执行：</span></span><br><span class="line">Upload.prototype.bindEvent = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> self = <span class="keyword">this</span>; </span><br><span class="line">    <span class="keyword">this</span>.button1.onclick = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        self.currState.clickHandler1(); </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">this</span>.button2.onclick = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        self.currState.clickHandler2(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">//第四步中的代码有一些变化，我们把状态对应的逻辑行为放在 Upload 类中：</span></span><br><span class="line">Upload.prototype.sign = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plugin.sign(); </span><br><span class="line">    <span class="keyword">this</span>.currState = <span class="keyword">this</span>.signState; </span><br><span class="line">&#125;; </span><br><span class="line">Upload.prototype.uploading = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.button1.innerHTML = <span class="string">'正在上传，点击暂停'</span>; </span><br><span class="line">    <span class="keyword">this</span>.plugin.uploading(); </span><br><span class="line">    <span class="keyword">this</span>.currState = <span class="keyword">this</span>.uploadingState; </span><br><span class="line">&#125;; </span><br><span class="line">Upload.prototype.pause = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.button1.innerHTML = <span class="string">'已暂停，点击继续上传'</span>; </span><br><span class="line">    <span class="keyword">this</span>.plugin.pause(); </span><br><span class="line">    <span class="keyword">this</span>.currState = <span class="keyword">this</span>.pauseState; </span><br><span class="line">&#125;; </span><br><span class="line">Upload.prototype.done = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.button1.innerHTML = <span class="string">'上传完成'</span>; </span><br><span class="line">    <span class="keyword">this</span>.plugin.done(); </span><br><span class="line">    <span class="keyword">this</span>.currState = <span class="keyword">this</span>.doneState; </span><br><span class="line">&#125;; </span><br><span class="line">Upload.prototype.error = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.button1.innerHTML = <span class="string">'上传失败'</span>; </span><br><span class="line">    <span class="keyword">this</span>.currState = <span class="keyword">this</span>.errorState; </span><br><span class="line">&#125;; </span><br><span class="line">Upload.prototype.del = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plugin.del(); </span><br><span class="line">    <span class="keyword">this</span>.dom.parentNode.removeChild( <span class="keyword">this</span>.dom ); </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">//第五步，工作略显乏味，我们要编写各个状态类的实现。值得注意的是，我们使用了StateFactory，从而避免因为 JavaScript 中没有抽象类所带来的问题。</span></span><br><span class="line"><span class="keyword">var</span> StateFactory = (<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> State = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125;; </span><br><span class="line">    State.prototype.clickHandler1 = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'子类必须重写父类的 clickHandler1 方法'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">    State.prototype.clickHandler2 = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'子类必须重写父类的 clickHandler2 方法'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"> param </span>)</span>&#123; </span><br><span class="line">        <span class="keyword">var</span> F = <span class="function"><span class="keyword">function</span>(<span class="params"> uploadObj </span>)</span>&#123; </span><br><span class="line">            <span class="keyword">this</span>.uploadObj = uploadObj; </span><br><span class="line">        &#125;; </span><br><span class="line">        F.prototype = <span class="keyword">new</span> State(); </span><br><span class="line">        <span class="keyword">for</span> ( <span class="keyword">var</span> i <span class="keyword">in</span> param )&#123; </span><br><span class="line">            F.prototype[ i ] = param[ i ]; </span><br><span class="line">        &#125; </span><br><span class="line">        <span class="keyword">return</span> F; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;)(); </span><br><span class="line"><span class="keyword">var</span> SignState = StateFactory(&#123;</span><br><span class="line">    clickHandler1: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'扫描中，点击无效...'</span> ); </span><br><span class="line">    &#125;, </span><br><span class="line">    clickHandler2: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'文件正在上传中，不能删除'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> UploadingState = StateFactory(&#123; </span><br><span class="line">    clickHandler1: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">this</span>.uploadObj.pause(); </span><br><span class="line">    &#125;, </span><br><span class="line">    clickHandler2: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'文件正在上传中，不能删除'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> PauseState = StateFactory(&#123; </span><br><span class="line">    clickHandler1: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">this</span>.uploadObj.uploading(); </span><br><span class="line">    &#125;, </span><br><span class="line">    clickHandler2: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">this</span>.uploadObj.del(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> DoneState = StateFactory(&#123; </span><br><span class="line">    clickHandler1: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'文件已完成上传, 点击无效'</span> ); </span><br><span class="line">    &#125;, </span><br><span class="line">    clickHandler2: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">this</span>.uploadObj.del(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> ErrorState = StateFactory(&#123; </span><br><span class="line">    clickHandler1: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'文件上传失败, 点击无效'</span> ); </span><br><span class="line">    &#125;, </span><br><span class="line">    clickHandler2: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">this</span>.uploadObj.del(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<h2 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h2><p>到这里我们已经学习了两个状态模式的例子，现在是时候来总结状态模式的优缺点了。</p>
<p>状态 模式的优点如下。 </p>
<p>1、状态模式定义了状态与行为之间的关系，并将它们封装在一个类里。通过增加新的状态 类，很容易增加新的状态和转换。</p>
<p>2、 避免 Context 无限膨胀，状态切换的逻辑被分布在状态类中，也去掉了 Context 中原本过 多的条件分支。 </p>
<p>3、用对象代替字符串来记录当前状态，使得状态的切换更加一目了然。</p>
<p>4、Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。 </p>
<p>状态模式的缺点是会在系统中定义许多状态类，编写 20 个状态类是一项枯燥乏味的工作， 而且系统中会因此而增加不少对象。另外，由于逻辑分散在状态类中，虽然避开了不受欢迎的条 件分支语句，但也造成了逻辑分散的问题，我们无法在一个地方就看出整个状态机的逻辑。</p>
<p>性能优化点：1、有两种选择来管理 state 对象的创建和销毁。第一种是仅当 state 对象被需要时才创建并 随后销毁，另一种是一开始就创建好所有的状态对象，并且始终不销毁它们。如果 state 对象比较庞大，可以用第一种方式来节省内存，这样可以避免创建一些不会用到的对象 并及时地回收它们。但如果状态的改变很频繁，最好一开始就把这些 state 对象都创建出 来，也没有必要销毁它们，因为可能很快将再次用到它们。</p>
<p>2、在本章的例子中，我们为每个 Context 对象都创建了一组 state 对象，实际上这些 state 对象之间是可以共享的，各 Context 对象可以共享一个 state 对象，这也是享元模式的应 用场景之一。</p>
<h2 id="JS版本状态机"><a href="#JS版本状态机" class="headerlink" title="JS版本状态机"></a>JS版本状态机</h2><p>前面两个示例都是模拟传统面向对象语言的状态模式实现，我们为每种状态都定义一个状态 子类，然后在 Context 中持有这些状态对象的引用，以便把 currState 设置为当前的状态对象。 </p>
<p>状态模式是状态机的实现之一，但在 JavaScript 这种“无类”语言中，没有规定让状态对象 一定要从类中创建而来。另外一点，JavaScript 可以非常方便地使用委托技术，并不需要事先让 一个对象持有另一个对象。下面的状态机选择了通过 Function.prototype.call 方法直接把请求委 托给某个字面量对象来执行。  </p>
<p>下面改写电灯的例子，来展示这种更加轻巧的做法：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Light = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.currState = FSM.off; <span class="comment">// 设置当前状态</span></span><br><span class="line">    <span class="keyword">this</span>.button = <span class="literal">null</span>; </span><br><span class="line">&#125;; </span><br><span class="line">Light.prototype.init = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> button = <span class="built_in">document</span>.createElement( <span class="string">'button'</span> ), </span><br><span class="line">        self = <span class="keyword">this</span>; </span><br><span class="line">    button.innerHTML = <span class="string">'已关灯'</span>; </span><br><span class="line">    <span class="keyword">this</span>.button = <span class="built_in">document</span>.body.appendChild( button ); </span><br><span class="line">    <span class="keyword">this</span>.button.onclick = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        self.currState.buttonWasPressed.call( self ); <span class="comment">// 把请求委托给 FSM 状态机</span></span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> FSM = &#123; </span><br><span class="line">    off: &#123; </span><br><span class="line">        buttonWasPressed: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">            <span class="built_in">console</span>.log( <span class="string">'关灯'</span> ); </span><br><span class="line">            <span class="keyword">this</span>.button.innerHTML = <span class="string">'下一次按我是开灯'</span>; </span><br><span class="line">            <span class="keyword">this</span>.currState = FSM.on; </span><br><span class="line">        &#125; </span><br><span class="line">    &#125;, </span><br><span class="line">    on: &#123; </span><br><span class="line">        buttonWasPressed: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">            <span class="built_in">console</span>.log( <span class="string">'开灯'</span> ); </span><br><span class="line">            <span class="keyword">this</span>.button.innerHTML = <span class="string">'下一次按我是关灯'</span>; </span><br><span class="line">            <span class="keyword">this</span>.currState = FSM.off; </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> light = <span class="keyword">new</span> Light(); </span><br><span class="line">light.init();</span><br></pre></td></tr></table></figure>

<p>状态模式也许是被大家低估的 模式之一。实际上，通过状态模式重构代码之后，很多杂乱无章的代码会变得清晰。</p>
<h1 id="适配器模式"><a href="#适配器模式" class="headerlink" title="适配器模式"></a>适配器模式</h1><p>适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后，原本 由于接口不兼容而不能工作的两个软件实体可以一起工作。 适配器的别名是包装器（wrapper），这是一个相对简单的模式。</p>
<p>在程序开发中有许多这样的 场景：当我们试图调用模块或者对象的某个接口时，却发现这个接口的格式并不符合目前的需求。 这时候有两种解决办法，第一种是修改原来的接口实现，但如果原来的模块很复杂，或者我们拿 到的模块是一段别人编写的经过压缩的代码，修改原接口就显得不太现实了。第二种办法是创建 一个适配器，将原接口转换为客户希望的另一个接口，客户只需要和适配器打交道。</p>
<p>适配器模式是一对相对简单的模式。在本书提到的设计模式中，有一些模式跟适配器模式的 结构非常相似，比如装饰者模式、代理模式和外观模式（参见第 19 章）。这几种模式都属于“包 装模式”，都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。 </p>
<p>1、适配器模式主要用来解决两个已有接口之间不匹配的问题，它不考虑这些接口是怎样实 现的，也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口，就能够 使它们协同作用。 </p>
<p>2、装饰者模式和代理模式也不会改变原有对象的接口，但装饰者模式的作用是为了给对象 增加功能。装饰者模式常常形成一条长的装饰链，而适配器模式通常只包装一次。代理 模式是为了控制对对象的访问，通常也只包装一次。 </p>
<p>3、外观模式的作用倒是和适配器比较相似，有人把外观模式看成一组对象的适配器，但外 观模式最显著的特点是定义了一个新的接口。</p>

      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/03/%E4%B9%A6%E5%8D%95/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/03/%E4%B9%A6%E5%8D%95/" class="post-title-link" itemprop="url">JavaScript的设计模式（三）</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
    
              <time title="创建时间：2021-11-03 20:02:33" itemprop="dateCreated datePublished" datetime="2021-11-03T20:02:33+08:00">2021-11-03</time>
            </span>
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="far fa-calendar-check"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2021-11-04 17:24:09" itemprop="dateModified" datetime="2021-11-04T17:24:09+08:00">2021-11-04</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/" itemprop="url" rel="index"><span itemprop="name">设计模式</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>本博客参考曾探所著&lt;&lt;JavaScript设计模式与开发实践&gt;&gt;，仅为个人学习总结，侵删。</p>
<h1 id="装饰器模式"><a href="#装饰器模式" class="headerlink" title="装饰器模式"></a>装饰器模式</h1><p>在程序开发中，许多时候都并不希望某个类天 生就非常庞大，一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地 给某个对象添加一些额外的职责，而不会影响从这个类中派生的其他对象。 </p>
<p>在传统的面向对象语言中，给对象添加功能常常使用继承的方式，但是继承的方式并不灵活， 还会带来许多问题：一方面会导致超类和子类之间存在强耦合性，当超类改变时，子类也会随之 改变；另一方面，继承这种功能复用方式通常被称为“白箱复用”，“白箱”是相对可见性而言的， 在继承方式中，超类的内部细节是对子类可见的，继承常常被认为破坏了封装性。  使用继承还会带来另外一个问题，在完成一些功能复用的同时，有可能创建出大量的子类， 使子类的数量呈爆炸性增长。</p>
<p>这种给对象动态地增加职责的方式称为装 饰者（decorator）模式。装饰者模式能够在不改 变对象自身的基础上，在程序运行期间给对象 动态地添加职责。跟继承相比，装饰者是一种 更轻便灵活的做法，这是一种“即用即付”的 方式，</p>
<p>首先要提出来的是，作为一门解释执行的语言，给 JavaScript 中的对象动态添加或者改变职 责是一件再简单不过的事情，虽然这种做法改动了对象自身，跟传统定义中的装饰者模式并不一 样，但这无疑更符合 JavaScript 的语言特色。代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> obj = &#123; </span><br><span class="line">    name: <span class="string">'sven'</span>, </span><br><span class="line">    address: <span class="string">'深圳市'</span> </span><br><span class="line"> &#125;; </span><br><span class="line">obj.address = obj.address + <span class="string">'福田区'</span>;</span><br></pre></td></tr></table></figure>

<p>传统面向对象语言中的装饰者模式在 JavaScript 中适用的场景并不多，如上面代码所示，通 常我们并不太介意改动对象自身。尽管如此，本节我们还是稍微模拟一下传统面向对象语言中的 装饰者模式实现。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//下面来看代码实现，首先是原始的飞机类：</span></span><br><span class="line"><span class="keyword">var</span> Plane = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125; </span><br><span class="line">Plane.prototype.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射普通子弹'</span> ); </span><br><span class="line">&#125; </span><br><span class="line"><span class="comment">//接下来增加两个装饰类，分别是导弹和原子弹：</span></span><br><span class="line"><span class="keyword">var</span> MissileDecorator = <span class="function"><span class="keyword">function</span>(<span class="params"> plane </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plane = plane; </span><br><span class="line">&#125; </span><br><span class="line">MissileDecorator.prototype.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plane.fire(); </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射导弹'</span> ); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> AtomDecorator = <span class="function"><span class="keyword">function</span>(<span class="params"> plane </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plane = plane; </span><br><span class="line">&#125; </span><br><span class="line">AtomDecorator.prototype.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.plane.fire(); </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射原子弹'</span> ); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>导弹类和原子弹类的构造函数都接受参数 plane 对象，并且保存好这个参数，在它们的 fire 方法中，除了执行自身的操作之外，还调用 plane 对象的 fire 方法。 </p>
<p>这种给对象动态增加职责的方式，并没有真正地改动对象自身，而是将对象放入另一个对象 之中，这些对象以一条链的方式进行引用，形成一个聚合对象。这些对象都拥有相同的接口（fire 方法），当请求达到链中的某个对象时，这个对象会执行自身的操作，随后把请求转发给链中的 下一个对象。 因为装饰者对象和它所装饰的对象拥有一致的接口，所以它们对使用该对象的客户来说是透 明的，被装饰的对象也并不需要了解它曾经被装饰过，这种透明性使得我们可以递归地嵌套任意 多个装饰者对象，</p>
<p>装饰器其实也是包装器：从功能上而言，decorator 能很好地描述这 个模式，但从结构上看，wrapper 的说法更加 贴切。装饰者模式将一个对象嵌入另一个对象 之中，实际上相当于这个对象被另一个对象包 装起来，形成一条包装链。请求随着这条链依 次传递到所有的对象，每个对象都有处理这条 请求的机会。</p>
<h2 id="装饰函数"><a href="#装饰函数" class="headerlink" title="装饰函数"></a>装饰函数</h2><p>JavaScript 语言动态改变对象相当容易，我们可以直接改写对象或者对象的某个方法，并不 需要使用“类”来实现装饰者模式，代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> plane = &#123; </span><br><span class="line">    fire: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'发射普通子弹'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> missileDecorator = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射导弹'</span> ); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> atomDecorator = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'发射原子弹'</span> ); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> fire1 = plane.fire; </span><br><span class="line">plane.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    fire1(); </span><br><span class="line">    missileDecorator(); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> fire2 = plane.fire; </span><br><span class="line">plane.fire = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    fire2(); </span><br><span class="line">    atomDecorator(); </span><br><span class="line">&#125; </span><br><span class="line">plane.fire(); </span><br><span class="line"><span class="comment">// 分别输出： 发射普通子弹、发射导弹、发射原子弹</span></span><br></pre></td></tr></table></figure>

<p>在 JavaScript 中，几乎一切都是对象，其中函数又被称为一等对象。在平时的开发工作中， 也许大部分时间都在和函数打交道。在 JavaScript 中可以很方便地给某个对象扩展属性和方法， 但却很难在不改动某个函数源代码的情况下，给该函数添加一些额外的功能。</p>
<p>在代码的运行期间， 我们很难切入某个函数的执行环境。 要想为函数添加一些功能，最简单粗暴的方式就是直接改写该函数，但这是最差的办法，直 接违反了开放-封闭原则</p>
<p>很多时候我们不想去碰原函数，也许原函数是由其他同事编写的，里面的实现非常杂乱。甚 至在一个古老的项目中，这个函数的源代码被隐藏在一个我们不愿碰触的阴暗角落里。现在需要 一个办法，在不改变函数源代码的情况下，能给函数增加功能，这正是开放-封闭原则给我们指出的光明道路。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//其实在 15.3 节的代码中，我们已经找到了一种答案，通过保存原引用的方式就可以改写某个函数：</span></span><br><span class="line"><span class="keyword">var</span> a = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    alert (<span class="number">1</span>); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> _a = a; </span><br><span class="line">a = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    _a(); </span><br><span class="line">    alert (<span class="number">2</span>); </span><br><span class="line">&#125; </span><br><span class="line">a(); </span><br><span class="line"><span class="comment">/*这是实际开发中很常见的一种做法，比如我们想给 window 绑定 onload 事件，但是又不确定</span></span><br><span class="line"><span class="comment">这个事件是不是已经被其他人绑定过，为了避免覆盖掉之前的 window.onload 函数中的行为，我</span></span><br><span class="line"><span class="comment">们一般都会先保存好原先的 window.onload，把它放入新的 window.onload 里执行：*/</span></span><br><span class="line"><span class="built_in">window</span>.onload = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    alert (<span class="number">1</span>); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">var</span> _onload = <span class="built_in">window</span>.onload || <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125;; </span><br><span class="line"><span class="built_in">window</span>.onload = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    _onload(); </span><br><span class="line">    alert (<span class="number">2</span>); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这样的代码当然是符合开放封闭原则的，我们在增加新功能的时候，确实没有修改原来的 window.onload 代码，但是这种方式存在以下两个问题。</p>
<p>1、必须维护_onload 这个中间变量，虽然看起来并不起眼，但如果函数的装饰链较长，或者 需要装饰的函数变多，这些中间变量的数量也会越来越多。</p>
<p>2、 其实还遇到了 this 被劫持的问题，在 window.onload 的例子中没有这个烦恼，是因为调用 普通函数_onload 时，this 也指向 window，跟调用 window.onload 时一样（函数作为对象的 方法被调用时，this 指向该对象，所以此处 this 也只指向 window）。现在把 window.onload 换成 document.getElementById，</p>
<h2 id="AOP装饰函数"><a href="#AOP装饰函数" class="headerlink" title="AOP装饰函数"></a>AOP装饰函数</h2><p>首先给出Function.prototype.before 方法和 Function.prototype.after 方法：这种AOP编程的方式和洋葱模型的before、after插件执行方式类似：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Function</span>.prototype.before = <span class="function"><span class="keyword">function</span>(<span class="params"> beforefn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> __self = <span class="keyword">this</span>; <span class="comment">// 保存原函数的引用</span></span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; <span class="comment">// 返回包含了原函数和新函数的"代理"函数</span></span><br><span class="line">        beforefn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); <span class="comment">// 执行新函数，且保证 this 不被劫持，新函数接受的参数</span></span><br><span class="line">        <span class="comment">// 也会被原封不动地传入原函数，新函数在原函数之前执行</span></span><br><span class="line">        <span class="keyword">return</span> __self.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); <span class="comment">// 执行原函数并返回原函数的执行结果，</span></span><br><span class="line"> <span class="comment">// 并且保证 this 不被劫持</span></span><br><span class="line">    &#125; </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="built_in">Function</span>.prototype.after = <span class="function"><span class="keyword">function</span>(<span class="params"> afterfn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> __self = <span class="keyword">this</span>; </span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">var</span> ret = __self.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); </span><br><span class="line">        afterfn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> );</span><br><span class="line">        <span class="keyword">return</span> ret; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>


      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  

  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="http://example.com/2021/11/03/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/JavaScript%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%B8%89%EF%BC%89/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/images/author.jpg">
      <meta itemprop="name" content="Li Yudong">
      <meta itemprop="description" content="">
    </span>
    
    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="Technological Blog">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/2021/11/03/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/JavaScript%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%B8%89%EF%BC%89/" class="post-title-link" itemprop="url">JavaScript的设计模式（三）</a>
        </h2>
    
        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
    
              <time title="创建时间：2021-11-03 20:02:33" itemprop="dateCreated datePublished" datetime="2021-11-03T20:02:33+08:00">2021-11-03</time>
            </span>
              <span class="post-meta-item">
                <span class="post-meta-item-icon">
                  <i class="far fa-calendar-check"></i>
                </span>
                <span class="post-meta-item-text">更新于</span>
                <time title="修改时间：2021-11-04 16:58:23" itemprop="dateModified" datetime="2021-11-04T16:58:23+08:00">2021-11-04</time>
              </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/" itemprop="url" rel="index"><span itemprop="name">设计模式</span></a>
                </span>
            </span>
    
          
    
        </div>
      </header>
    
    
    
    
    <div class="post-body" itemprop="articleBody">
    
      
          <h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>本博客参考曾探所著&lt;&lt;JavaScript设计模式与开发实践&gt;&gt;，仅为个人学习总结，侵删。</p>
<h1 id="组合模式"><a href="#组合模式" class="headerlink" title="组合模式"></a>组合模式</h1><p>组合模式就是用小的子对象来构建更大的对象，而这些小的子对象本身也许是由更 小的“孙对象”构成的。</p>
<p>我们在第 9 章命令模式中讲解过宏命令的结构和作用。宏命令对象包含了一组具体的子命令 对象，不管是宏命令对象，还是子命令对象，都有一个 execute 方法负责执行命令。现在回顾一 下这段安装在万能遥控器上的宏命令代码</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> closeDoorCommand = &#123; </span><br><span class="line">    execute: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'关门'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> openPcCommand = &#123; </span><br><span class="line">    execute: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'开电脑'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> openQQCommand = &#123; </span><br><span class="line">    execute: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'登录 QQ'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> MacroCommand = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">return</span> &#123; </span><br><span class="line">        commandsList: [], </span><br><span class="line">        add: <span class="function"><span class="keyword">function</span>(<span class="params"> command </span>)</span>&#123; </span><br><span class="line">            <span class="keyword">this</span>.commandsList.push( command ); </span><br><span class="line">        &#125;, </span><br><span class="line">        execute: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">            <span class="keyword">for</span> ( <span class="keyword">var</span> i = <span class="number">0</span>, command; command = <span class="keyword">this</span>.commandsList[ i++ ]; )&#123; </span><br><span class="line">                command.execute(); </span><br><span class="line">            &#125; </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> macroCommand = MacroCommand(); </span><br><span class="line">macroCommand.add( closeDoorCommand ); </span><br><span class="line">macroCommand.add( openPcCommand ); </span><br><span class="line">macroCommand.add( openQQCommand ); </span><br><span class="line">macroCommand.execute();</span><br></pre></td></tr></table></figure>

<p>通过观察这段代码，我们很容易发现，宏命令中包含了一组子命令，它们组成了一个树形结 构，这里是一棵结构非常简单的树，</p>
<p>其中，marcoCommand 被称为组合对象，closeDoorCommand、openPcCommand、openQQCommand 都是 叶对象。在 macroCommand 的 execute 方法里，并不执行真正的操作，而是遍历它所包含的叶对象， 把真正的 execute 请求委托给这些叶对象。 macroCommand 表现得像一个命令，但它实际上只是一组真正命令的“代理”。并非真正的代理， 虽然结构上相似，但 macroCommand 只负责传递请求给叶对象，它的目的不在于控制对叶对象的访问。</p>
<p>组合模式将对象组合成树形结构，以表示“部分-整体”的层次结构。 除了用来表示树形结 构之外，组合模式的另一个好处是通过对象的多态性表现，使得用户对单个对象和组合对象的使 用具有一致性，下面分别说明。 </p>
<p>1、表示树形结构。通过回顾上面的例子，我们很容易找到组合模式的一个优点：提供了一 种遍历树形结构的方案，通过调用组合对象的 execute 方法，程序会递归调用组合对象下 面的叶对象的 execute 方法，所以我们的万能遥控器只需要一次操作，便能依次完成关门、 打开电脑、登录 QQ 这几件事情。组合模式可以非常方便地描述对象部分整体层次结构。</p>
<p>2、利用对象多态性统一对待组合对象和单个对象。利用对象的多态性表现，可以使客户端 忽略组合对象和单个对象的不同。在组合模式中，客户将统一地使用组合结构中的所有 对象，而不需要关心它究竟是组合对象还是单个对象。</p>
<p>请求从上到下沿着树进行传递，直到树的尽头。作为客户，只需要关心树最顶层的组合对象， 客户只需要请求这个组合对象，请求便会沿着树往下传递，依次到达所有的叶对象。</p>
<p>基本对象可以被组合成更复杂的组合对象，组合对象又可以被组合， 这样不断递归下去，这棵树的结构可以支持任意多的复杂度。在树最终被构造完成之后，让整颗 树最终运转起来的步骤非常简单，只需要调用最上层对象的 execute 方法。每当对最上层的对象 进行一次请求时，实际上是在对整个树进行深度优先的搜索，</p>
<h2 id="抽象类与透明性"><a href="#抽象类与透明性" class="headerlink" title="抽象类与透明性"></a>抽象类与透明性</h2><p>前面说到，组合模式最大的优点在于可以一致地对待组合对象和基本对象。客户不需要知道 当前处理的是宏命令还是普通命令，只要它是一个命令，并且有 execute 方法，这个命令就可以 被添加到树中。 这种透明性带来的便利，在静态类型语言中体现得尤为明显。</p>
<p>比如在 Java 中，实现组合模 式的关键是 Composite 类和 Leaf 类都必须继承自一个 Compenent 抽象类。这个 Compenent 抽象类既 代表组合对象，又代表叶对象，它也能够保证组合对象和叶对象拥有同样名字的方法，从而可以 对同一消息都做出反馈。组合对象和叶对象的具体类型被隐藏在 Compenent 抽象类身后。  </p>
<p>针对 Compenent 抽象类来编写程序，客户操作的始终是 Compenent 对象，而不用去区分到底是 组合对象还是叶对象。所以我们往同一个对象里的 add 方法里，既可以添加组合对象，也可以添 加叶对象，代码如下</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Java 代码 </span></span><br><span class="line">public abstract <span class="class"><span class="keyword">class</span> <span class="title">Component</span></span>&#123; </span><br><span class="line">    <span class="comment">//add 方法，参数为 Component 类型</span></span><br><span class="line">    public <span class="keyword">void</span> add( Component child )&#123;&#125; </span><br><span class="line">    <span class="comment">//remove 方法，参数为 Component 类型</span></span><br><span class="line">    public <span class="keyword">void</span> remove( Component child )&#123;&#125; </span><br><span class="line">&#125; </span><br><span class="line">public <span class="class"><span class="keyword">class</span> <span class="title">Composite</span> <span class="keyword">extends</span> <span class="title">Component</span></span>&#123; </span><br><span class="line">    <span class="comment">//add 方法，参数为 Component 类型</span></span><br><span class="line">    public <span class="keyword">void</span> add( Component child )&#123;&#125; </span><br><span class="line">    <span class="comment">//remove 方法，参数为 Component 类型</span></span><br><span class="line">    public <span class="keyword">void</span> remove( Component child )&#123;&#125; </span><br><span class="line">&#125; </span><br><span class="line">public <span class="class"><span class="keyword">class</span> <span class="title">Leaf</span> <span class="keyword">extends</span> <span class="title">Component</span></span>&#123; </span><br><span class="line">    <span class="comment">//add 方法，参数为 Component 类型</span></span><br><span class="line">    public <span class="keyword">void</span> add( Component child )&#123; </span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> UnsupportedOperationException() <span class="comment">// 叶对象不能再添加子节点</span></span><br><span class="line">    &#125; </span><br><span class="line"> <span class="comment">//remove 方法，参数为 Component 类型</span></span><br><span class="line">    public <span class="keyword">void</span> remove( Component child )&#123; </span><br><span class="line">    &#125; </span><br><span class="line">&#125; </span><br><span class="line">public <span class="class"><span class="keyword">class</span> <span class="title">client</span>()</span>&#123; </span><br><span class="line">    public <span class="keyword">static</span> <span class="keyword">void</span> main( <span class="built_in">String</span> args[] )&#123; </span><br><span class="line">        Component root = <span class="keyword">new</span> Composite(); </span><br><span class="line">        Component c1 = <span class="keyword">new</span> Composite(); </span><br><span class="line">        Component c2 = <span class="keyword">new</span> Composite(); </span><br><span class="line">        Component leaf1 = <span class="keyword">new</span> Leaf(); </span><br><span class="line">        Component leaf2 = <span class="keyword">new</span> Leaf(); </span><br><span class="line">        root.add(c1); </span><br><span class="line">        root.add(c2); </span><br><span class="line">        c1.add(leaf1); </span><br><span class="line">        c1.add(leaf2); </span><br><span class="line">        root.remove(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>而在 JavaScript 这种动态类型语言中，对象的多态性是与生俱来的，也没有编译器去检查 变量的类型，所以我们通常不会去模拟一个“怪异”的抽象类，JavaScript 中实现组合模式的难 点在于要保证组合对象和叶对象对象拥有同样的方法，这通常需要用鸭子类型的思想对它们进行 接口检查。 在 JavaScript 中实现组合模式，看起来缺乏一些严谨性，我们的代码算不上安全，但能更快 速和自由地开发，这既是 JavaScript 的缺点，也是它的优点。</p>
<p>组合模式的透明性使得发起请求的客户不用去顾忌树中组合对象和叶对象的区别，但它们在 本质上有是区别的。 组合对象可以拥有子节点，叶对象下面就没有子节点， 所以我们也许会发生一些误操作， 比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加 add 方法，并且在调用这个方 法时，抛出一个异常来及时提醒客户，</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> MacroCommand = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">return</span> &#123; </span><br><span class="line">        commandsList: [], </span><br><span class="line">        add: <span class="function"><span class="keyword">function</span>(<span class="params"> command </span>)</span>&#123; </span><br><span class="line">            <span class="keyword">this</span>.commandsList.push( command ); </span><br><span class="line">        &#125;, </span><br><span class="line">        execute: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">            <span class="keyword">for</span> ( <span class="keyword">var</span> i = <span class="number">0</span>, command; command = <span class="keyword">this</span>.commandsList[ i++ ]; )&#123; </span><br><span class="line">                command.execute(); </span><br><span class="line">            &#125; </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> openTvCommand = &#123; </span><br><span class="line">    execute: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'打开电视'</span> ); </span><br><span class="line">    &#125;, </span><br><span class="line">    add: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'叶对象不能添加子节点'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> macroCommand = MacroCommand(); </span><br><span class="line">macroCommand.add( openTvCommand ); </span><br><span class="line">openTvCommand.add( macroCommand ) <span class="comment">// Uncaught Error: 叶对象不能添加子节点</span></span><br></pre></td></tr></table></figure>

<h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><p>1、组合模式不是父子关系 组合模式的树型结构容易让人误以为组合对象和叶对象是父子关系，这是不正确的。 组合模式是一种 HAS-A（聚合）的关系，而不是 IS-A。组合对象包含一组叶对象，但 Leaf 并不是 Composite 的子类。组合对象把请求委托给它所包含的所有叶对象，它们能够合作的关键 是拥有相同的接口。  为了方便描述，本章有时候把上下级对象称为父子节点，但大家要知道，它们并非真正意义 上的父子关系。 </p>
<p>2、对叶对象操作的一致性 组合模式除了要求组合对象和叶对象拥有相同的接口之外，还有一个必要条件，就是对一组 叶对象的操作必须具有一致性。 比如公司要给全体员工发放元旦的过节费 1000 块，这个场景可以运用组合模式，但如果公 司给今天过生日的员工发送一封生日祝福的邮件，组合模式在这里就没有用武之地了，除非先把 今天过生日的员工挑选出来。只有用一致的方式对待列表中的每个叶对象的时候，才适合使用组 合模式。</p>
<p>3、双向映射关系 发放过节费的通知步骤是从公司到各个部门，再到各个小组，最后到每个员工的邮箱里。这 本身是一个组合模式的好例子，但要考虑的一种情况是，也许某些员工属于多个组织架构。比如 某位架构师既隶属于开发组，又隶属于架构组，对象之间的关系并不是严格意义上的层次结构， 在这种情况下，是不适合使用组合模式的，该架构师很可能会收到两份过节费。</p>
<p>这种复合情况下我们必须给父节点和子节点建立双向映射关系，一个简单的方法是给小组和员 工对象都增加集合来保存对方的引用。但是这种相互间的引用相当复杂，而且对象之间产生了过多 的耦合性，修改或者删除一个对象都变得困难，此时我们可以引入中介者模式来管理这些对象</p>
<p>4、 用职责链模式提高组合模式性能。在组合模式中，如果树的结构比较复杂，节点数量很多，在遍历树的过程中，性能方面也许表现得不够理想。有时候我们确实可以借助一些技巧，在实际操作中避免遍历整棵树，有一种现成的方案是借助职责链模式。职责链模式一般需要我们手动去设置链条，但在组合模式中，父对象和子对象之间实际上形成了天然的职责链。让请求顺着链条从父对象往子对象传递，或者是反过来从子对象往父对象传递，直到遇到可以处理该请求的对象为止，这也是职责链模式的经典运用场景之一</p>
<h2 id="引用父对象"><a href="#引用父对象" class="headerlink" title="引用父对象"></a>引用父对象</h2><p>组合对象保存了它下面的子节点的引用，这是组合模式的特点，此 时树结构是从上至下的。但有时候我们需要在子节点上保持对父节点的引用，比如在组合模式中 使用职责链时，有可能需要让请求从子节点往父节点上冒泡传递。还有当我们删除某个文件的时 候，实际上是从这个文件所在的上层文件夹中删除该文件的。  </p>
<p>现在来改写扫描文件夹的代码，使得在扫描整个文件夹之前，我们可以先移除某一个具体的 文件。 首先改写 Folder 类和 File 类，在这两个类的构造函数中，增加 this.parent 属性，并且在调 用 add 方法的时候，正确设置文件或者文件夹的父节点：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Folder = <span class="function"><span class="keyword">function</span>(<span class="params"> name </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.name = name; </span><br><span class="line">    <span class="keyword">this</span>.parent = <span class="literal">null</span>; <span class="comment">//增加 this.parent 属性</span></span><br><span class="line">    <span class="keyword">this</span>.files = []; </span><br><span class="line">&#125;; </span><br><span class="line">Folder.prototype.add = <span class="function"><span class="keyword">function</span>(<span class="params"> file </span>)</span>&#123; </span><br><span class="line">    file.parent = <span class="keyword">this</span>; <span class="comment">//设置父对象</span></span><br><span class="line">    <span class="keyword">this</span>.files.push( file ); </span><br><span class="line">&#125;; </span><br><span class="line">Folder.prototype.scan = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'开始扫描文件夹: '</span> + <span class="keyword">this</span>.name ); </span><br><span class="line">    <span class="keyword">for</span> ( <span class="keyword">var</span> i = <span class="number">0</span>, file, files = <span class="keyword">this</span>.files; file = files[ i++ ]; )&#123; </span><br><span class="line">        file.scan(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">//接下来增加 Folder.prototype.remove 方法，表示移除该文件夹</span></span><br><span class="line">Folder.prototype.remove = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( !<span class="keyword">this</span>.parent )&#123; <span class="comment">//根节点或者树外的游离节点</span></span><br><span class="line">        <span class="keyword">return</span>; </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">for</span> ( <span class="keyword">var</span> files = <span class="keyword">this</span>.parent.files, l = files.length - <span class="number">1</span>; l &gt;=<span class="number">0</span>; l-- )&#123; </span><br><span class="line">        <span class="keyword">var</span> file = files[ l ]; </span><br><span class="line">        <span class="keyword">if</span> ( file === <span class="keyword">this</span> )&#123; </span><br><span class="line">            files.splice( l, <span class="number">1</span> ); </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">	在 File.prototype.remove 方法里，首先会判断 this.parent，如果 this.parent 为 null，那么这个文件夹要么是树的根节点，要么是还没有添加到树的游离节点，这时候没有节点需要从树中移除，我们暂且让 remove 方法直接 return，表示不做任何操作。</span></span><br><span class="line"><span class="comment">	如果 this.parent 不为 null，则说明该文件夹有父节点存在，此时遍历父节点中保存的子节点列表，删除想要删除的子节点。</span></span><br><span class="line"><span class="comment">File 类的实现基本一致</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">var</span> File = <span class="function"><span class="keyword">function</span>(<span class="params"> name </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.name = name; </span><br><span class="line">    <span class="keyword">this</span>.parent = <span class="literal">null</span>; </span><br><span class="line">&#125;; </span><br><span class="line">File.prototype.add = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'不能添加在文件下面'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line">File.prototype.scan = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'开始扫描文件: '</span> + <span class="keyword">this</span>.name ); </span><br><span class="line">&#125;; </span><br><span class="line">File.prototype.remove = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( !<span class="keyword">this</span>.parent )&#123; <span class="comment">//根节点或者树外的游离节点</span></span><br><span class="line">        <span class="keyword">return</span>; </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">for</span> ( <span class="keyword">var</span> files = <span class="keyword">this</span>.parent.files, l = files.length - <span class="number">1</span>; l &gt;=<span class="number">0</span>; l-- )&#123; </span><br><span class="line">        <span class="keyword">var</span> file = files[ l ]; </span><br><span class="line">        <span class="keyword">if</span> ( file === <span class="keyword">this</span> )&#123; </span><br><span class="line">            files.splice( l, <span class="number">1</span> ); </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">//下面测试一下我们的移除文件功能：</span></span><br><span class="line"><span class="keyword">var</span> folder = <span class="keyword">new</span> Folder( <span class="string">'学习资料'</span> ); </span><br><span class="line"><span class="keyword">var</span> folder1 = <span class="keyword">new</span> Folder( <span class="string">'JavaScript'</span> ); </span><br><span class="line"><span class="keyword">var</span> file1 = <span class="keyword">new</span> Folder ( <span class="string">'深入浅出 Node.js'</span> ); </span><br><span class="line">folder1.add( <span class="keyword">new</span> File( <span class="string">'JavaScript 设计模式与开发实践'</span> ) ); </span><br><span class="line">folder.add( folder1 ); </span><br><span class="line">folder.add( file1 ); </span><br><span class="line">folder1.remove(); <span class="comment">//移除文件夹</span></span><br><span class="line">folder.scan();</span><br></pre></td></tr></table></figure>

<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>组合模式如果运用得当，可以大大简化客户的代码。一般来说，组合模式适用于以下这两种 情况。</p>
<p>1、表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后，只需要通过请求树的最顶层对象，便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便，并且符合开放-封闭原则。</p>
<p>2、客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别， 客户在面对这棵树的时候，不用关心当前正在处理的对象是组合对象还是叶对象，也就 不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情， 这是组合模式最重要的能力。</p>
<p>组合模式可以让我们使用树形方式创 建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下，我们都 可以忽略掉组合对象和单个对象之间的差别，从而用一致的方式来处理它们。 然而，组合模式并不是完美的，它可能会产生一个这样的系统：系统中的每个对象看起来都 与其他对象差不多。它们的区别只有在运行的时候会才会显现出来，这会使代码难以理解。此外， 如果通过组合模式创建了太多的对象，那么这些对象可能会让系统负担不起。</p>
<h1 id="模板方法模式"><a href="#模板方法模式" class="headerlink" title="模板方法模式"></a>模板方法模式</h1><p>在 JavaScript 开发中用到继承的场景其实并不是很多，很多时候我们都喜欢用 mix-in 的方式 给对象扩展属性。但这不代表继承在 JavaScript 里没有用武之地，虽然没有真正的类和继承机制， 但我们可以通过原型 prototype 来变相地实现继承。 不过本章并非要讨论继承，而是讨论一种基于继承的设计模式——模板方法（Template Method） 模式</p>
<p>模板方法模式是一种只需使用继承就可以实现的非常简单的模式。 模板方法模式由两部分结构组成，第一部分是抽象父类，第二部分是具体的实现子类。通常 在抽象父类中封装了子类的算法框架，包括实现一些公共方法以及封装子类中所有方法的执行顺 序。子类通过继承这个抽象类，也继承了整个算法结构，并且可以选择重写父类的方法。</p>
<h2 id="抽象类"><a href="#抽象类" class="headerlink" title="抽象类"></a>抽象类</h2><p>JavaScript 并没有从语法层面提供对抽象类的支持。抽象类的第一个作用是隐藏对象的具 体类型，由于 JavaScript 是一门“类型模糊”的语言，所以隐藏对象的类型在 JavaScript 中并 不重要。</p>
<p>另一方面，当我们在 JavaScript 中使用原型继承来模拟传统的类式继承时，并没有编译器帮 助我们进行任何形式的检查，我们也没有办法保证子类会重写父类中的“抽象方法”。</p>
<p>在 Java 中编译器会保证子类会重写父类中的抽象方法，但在 JavaScript 中却没有进行这些检 查工作。我们在编写代码的时候得不到任何形式的警告，完全寄托于程序员的记忆力和自觉性是 很危险的，特别是当我们使用模板方法模式这种完全依赖继承而实现的设计模式时。 下面提供两种变通的解决方案：</p>
<p>1、用鸭子类型来模拟接口检查，以便确保子类中确实重写了父类的方法。但模 拟接口检查会带来不必要的复杂性，而且要求程序员主动进行这些接口检查，这就要求 我们在业务代码中添加一些跟业务逻辑无关的代码</p>
<p>2、是让 Beverage.prototype.brew 等方法直接抛出一个异常，如果因为粗心忘记编 写 Coffee.prototype.brew 方法，那么至少我们会在程序运行时得到一个错误：</p>
<h2 id="钩子方法"><a href="#钩子方法" class="headerlink" title="钩子方法"></a>钩子方法</h2><p>通过模板方法模式，我们在父类中封装了子类的算法框架。这些算法框架在正常状态下是适 用于大多数子类的，但如果有一些特别“个性”的子类呢？</p>
<p>钩子方法（hook）可以用来解决这个问题，放置钩子是隔离变化的一种常见手段。我们在父 类中容易变化的地方放置钩子，钩子可以有一个默认的实现，究竟要不要“挂钩”，这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤，也就是程序接下来的走向，这样一来，程序就拥有了变化的可能。 在这个例子里，我们把挂钩的名字定为 customerWantsCondiments，接下来将挂钩放入 Beverage 类，看看我们如何得到一杯不需要糖和牛奶的咖啡，代码如下</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Beverage = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125;; </span><br><span class="line">Beverage.prototype.boilWater = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'把水煮沸'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line">Beverage.prototype.brew = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'子类必须重写 brew 方法'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line">Beverage.prototype.pourInCup = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'子类必须重写 pourInCup 方法'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line">Beverage.prototype.addCondiments = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'子类必须重写 addCondiments 方法'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line">Beverage.prototype.customerWantsCondiments = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>; <span class="comment">// 默认需要调料</span></span><br><span class="line">&#125;; </span><br><span class="line">Beverage.prototype.init = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.boilWater(); </span><br><span class="line">    <span class="keyword">this</span>.brew(); </span><br><span class="line">    <span class="keyword">this</span>.pourInCup(); </span><br><span class="line">    <span class="keyword">if</span> ( <span class="keyword">this</span>.customerWantsCondiments() )&#123; <span class="comment">// 如果挂钩返回 true，则需要调料</span></span><br><span class="line">        <span class="keyword">this</span>.addCondiments(); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> CoffeeWithHook = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125;; </span><br><span class="line">CoffeeWithHook.prototype = <span class="keyword">new</span> Beverage(); </span><br><span class="line">CoffeeWithHook.prototype.brew = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'用沸水冲泡咖啡'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line">CoffeeWithHook.prototype.pourInCup = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'把咖啡倒进杯子'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line">CoffeeWithHook.prototype.addCondiments = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'加糖和牛奶'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line">CoffeeWithHook.prototype.customerWantsCondiments = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">window</span>.confirm( <span class="string">'请问需要调料吗？'</span> ); </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> coffeeWithHook = <span class="keyword">new</span> CoffeeWithHook(); </span><br><span class="line">coffeeWithHook.init();</span><br></pre></td></tr></table></figure>

<h2 id="好莱坞原则"><a href="#好莱坞原则" class="headerlink" title="好莱坞原则"></a>好莱坞原则</h2><p>学习完模板方法模式之后，我们要引入一个新的设计原则——著名的“好莱坞原则”。 </p>
<p>好莱坞无疑是演员的天堂，但好莱坞也有很多找不到工作的新人演员，许多新人演员在好莱 坞把简历递给演艺公司之后就只有回家等待电话。有时候该演员等得不耐烦了，给演艺公司打电 话询问情况，演艺公司往往这样回答：“不要来找我，我会给你打电话。” </p>
<p>在设计中，这样的规则就称为好莱坞原则。在这一原则的指导下，我们允许底层组件将自己 挂钩到高层组件中，而高层组件会决定什么时候、以何种方式去使用这些底层组件，高层组件对 待底层组件的方式，跟演艺公司对待新人演员一样，都是“别调用我们，我们会调用你”。 </p>
<p>模板方法模式是好莱坞原则的一个典型使用场景，它与好莱坞原则的联系非常明显，当我们 用模板方法模式编写一个程序时，就意味着子类放弃了对自己的控制权，而是改为父类通知子类， 哪些方法应该在什么时候被调用。作为子类，只负责提供一些设计上的细节。 除此之外，好莱坞原则还常常应用于其他模式和场景，例如发布-订阅模式和回调函数：</p>
<p>1、在发布—订阅模式中，发布者会把消息推送给订阅者，这取代了原先不断去 fetch 消息的形式。 例如假设我们乘坐出租车去一个不了解的地方，除了每过 5 秒钟就问司机“是否到达目的地”之 外，还可以在车上美美地睡上一觉，然后跟司机说好，等目的地到了就叫醒你。这也相当于好莱 坞原则中提到的“别调用我们，我们会调用你”。</p>
<p>2、在 ajax 异步请求中，由于不知道请求返回的具体时间，而通过轮询去判断是否返回数据，这 显然是不理智的行为。所以我们通常会把接下来的操作放在回调函数中，传入发起 ajax 异步请求 的函数。当数据返回之后，这个回调函数才被执行，这也是好莱坞原则的一种体现。把需要执行 的操作封装在回调函数里，然后把主动权交给另外一个函数。至于回调函数什么时候被执行，则 是另外一个函数控制的。</p>
<h2 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h2><p>模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语 言中，一个运用了模板方法模式的程序中，子类的方法种类和执行顺序都是不变的，所以我们把 这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的，于是我们把这 部分变化的逻辑封装到子类中。通过增加新的子类，我们便能给系统增加新的功能，并不需要改 动抽象父类以及其他子类，这也是符合开放封闭原则的。 </p>
<p>但在 JavaScript 中，我们很多时候都不需要依样画瓢地去实现一个模版方法模式，高阶函数（一个函数可以接收另一个函数做参数，该函数就可以称为高阶函数）是更好的选择。如下代码所示：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Beverage = <span class="function"><span class="keyword">function</span>(<span class="params"> param </span>)</span>&#123;</span><br><span class="line">    <span class="keyword">var</span> boilWater = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'把水煮沸'</span> ); </span><br><span class="line">    &#125;; </span><br><span class="line">    <span class="keyword">var</span> brew = param.brew || <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'必须传递 brew 方法'</span> ); </span><br><span class="line">    &#125;; </span><br><span class="line">    <span class="keyword">var</span> pourInCup = param.pourInCup || <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'必须传递 pourInCup 方法'</span> ); </span><br><span class="line">    &#125;; </span><br><span class="line">    <span class="keyword">var</span> addCondiments = param.addCondiments || <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>( <span class="string">'必须传递 addCondiments 方法'</span> ); </span><br><span class="line">    &#125;; </span><br><span class="line">    <span class="keyword">var</span> F = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;&#125;; </span><br><span class="line">    F.prototype.init = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        boilWater(); </span><br><span class="line">        brew(); </span><br><span class="line">        pourInCup(); </span><br><span class="line">        addCondiments(); </span><br><span class="line">    &#125;; </span><br><span class="line">    <span class="keyword">return</span> F;</span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> Coffee = Beverage(&#123; </span><br><span class="line">    brew: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'用沸水冲泡咖啡'</span> ); </span><br><span class="line">    &#125;, </span><br><span class="line">    pourInCup: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'把咖啡倒进杯子'</span> ); </span><br><span class="line">    &#125;, </span><br><span class="line">    addCondiments: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'加糖和牛奶'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> Tea = Beverage(&#123;</span><br><span class="line">    brew: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'用沸水浸泡茶叶'</span> ); </span><br><span class="line">    &#125;, </span><br><span class="line">    pourInCup: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'把茶倒进杯子'</span> ); </span><br><span class="line">    &#125;, </span><br><span class="line">    addCondiments: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'加柠檬'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> coffee = <span class="keyword">new</span> Coffee(); </span><br><span class="line">coffee.init(); </span><br><span class="line"><span class="keyword">var</span> tea = <span class="keyword">new</span> Tea(); </span><br><span class="line">tea.init();</span><br></pre></td></tr></table></figure>

<p>在这段代码中，我们把 brew、pourInCup、addCondiments 这些方法依次传入 Beverage 函数， Beverage 函数被调用之后返回构造器 F。F 类中包含了“模板方法”F.prototype.init。跟继承得到的效果一样，该“模板方法”里依然封装了饮料子类的算法框架</p>
<h1 id="享元模式"><a href="#享元模式" class="headerlink" title="享元模式"></a>享元模式</h1><p>享元（flyweight）模式是一种用于性能优化的模式，“fly”在这里是苍蝇的意思，意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。 如果系统中因为创建了大量类似的对象而导致内存占用过高，享元模式就非常有用了。在 JavaScript 中，浏览器特别是移动端的浏览器分配的内存并不算多，如何节省内存就成了一件非 常有意义的事情。 享元模式的概念初听起来并不太好理解，所以在深入讲解之前，我们先看一个例子。</p>
<p>假设有个内衣工厂，目前的产品有 50 种男式内衣和 50 种女士内衣，为了推销产品，工厂决 定生产一些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要 50 个男模特和 50 个女 模特，然后让他们每人分别穿上一件内衣来拍照。</p>
<p>要得到一张照片，每次都需要传入 sex 和 underwear 参数，如上所述，现在一共有 50 种男内 衣和 50 种女内衣，所以一共会产生 100 个对象。如果将来生产了 10000 种内衣，那这个程序可 能会因为存在如此多的对象已经提前崩溃。 下面我们来考虑一下如何优化这个场景。虽然有 100 种内衣，但很显然并不需要 50 个男 模特和 50 个女模特。其实男模特和女模特各自有一个就足够了，他们可以分别穿上不同的内衣来拍照。现在来改写一下代码，既然只需要区别男女模特，那我们先把 underwear 参数从构造函数中移除，构造函数只接收 sex 参数：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> Model = <span class="function"><span class="keyword">function</span>(<span class="params"> sex </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.sex = sex; </span><br><span class="line">&#125;; </span><br><span class="line">Model.prototype.takePhoto = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="string">'sex= '</span> + <span class="keyword">this</span>.sex + <span class="string">' underwear='</span> + <span class="keyword">this</span>.underwear); </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> maleModel = <span class="keyword">new</span> Model( <span class="string">'male'</span> ), </span><br><span class="line">    femaleModel = <span class="keyword">new</span> Model( <span class="string">'female'</span> ); </span><br><span class="line"><span class="keyword">for</span> ( <span class="keyword">var</span> i = <span class="number">1</span>; i &lt;= <span class="number">50</span>; i++ )&#123; </span><br><span class="line">    maleModel.underwear = <span class="string">'underwear'</span> + i; </span><br><span class="line">    maleModel.takePhoto(); </span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">for</span> ( <span class="keyword">var</span> j = <span class="number">1</span>; j &lt;= <span class="number">50</span>; j++ )&#123; </span><br><span class="line">    femaleModel.underwear = <span class="string">'underwear'</span> + j; </span><br><span class="line">    femaleModel.takePhoto(); </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>享元模式要求将对象的属性划分为内部状态与外部 状态（状态在这里通常指属性）。享元模式的目标是尽量减少共享对象的数量，关于如何划分内 部状态和外部状态，</p>
<p>剥离了外部状态的对象成为共享对象，外部状态在必要时被传入共享对象来组装成一个完整 的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间，但却可以大大减少系 统中的对象数量，相比之下，这点时间或许是微不足道的。因此，享元模式是一种用时间换空间 的优化模式。</p>
<h2 id="通用结构"><a href="#通用结构" class="headerlink" title="通用结构"></a>通用结构</h2><p>我们通过构造函数显式 new 出了男女两个 model 对象，在其他系统中，也许并不是一开始 就需要所有的共享对象。 给 model 对象手动设置了 underwear 外部状态，在更复杂的系统中，这不是一个最好的方 式，因为外部状态可能会相当复杂，它们与共享对象的联系会变得困难。 </p>
<p>我们通过一个对象工厂来解决第一个问题，只有当某种共享对象被真正需要时，它才从工厂 中被创建出来。对于第二个问题，可以用一个管理器来记录对象相关的外部状态，使这些外部状 态通过某个钩子和共享对象联系起来。</p>
<h2 id="文件上传"><a href="#文件上传" class="headerlink" title="文件上传"></a>文件上传</h2><p>文件上传功能虽然可以选 择依照队列，一个一个地排队上传，但也支持同时选择 2000 个文件。每一个文件都对应着一个 JavaScript 上传对象的创建，在第一版开发中，的确往程序里同时 new 了 2000 个 upload 对象，结 果可想而知，Chrome 中还勉强能够支撑，IE 下直接进入假死状态。 项目支持好几种上传方式，比如浏览器插件、Flash 和表单上传等，为了简化例子，我们先 假设只有插件和 Flash 这两种。</p>
<p>基本的代码上传逻辑如下：当用户选择了文件之后，插件和 Flash 都会通知调用 Window 下的一个全局 JavaScript 函数，它的名字是 startUpload，用户选择的文件列表被组合成一个数组 files 塞进该函数的参数列表里，代码如下：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> id = <span class="number">0</span>; </span><br><span class="line"><span class="built_in">window</span>.startUpload = <span class="function"><span class="keyword">function</span>(<span class="params"> uploadType, files </span>)</span>&#123; <span class="comment">// uploadType 区分是控件还是 flash </span></span><br><span class="line">    <span class="keyword">for</span> ( <span class="keyword">var</span> i = <span class="number">0</span>, file; file = files[ i++ ]; )&#123; </span><br><span class="line">        <span class="keyword">var</span> uploadObj = <span class="keyword">new</span> Upload( uploadType, file.fileName, file.fileSize ); </span><br><span class="line">        uploadObj.init( id++ ); <span class="comment">// 给 upload 对象设置一个唯一的 id </span></span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">当用户选择完文件之后，startUpload 函数会遍历 files 数组来创建对应的 upload 对象。接下</span></span><br><span class="line"><span class="comment">来定义 Upload 构造函数，它接受 3 个参数，分别是插件类型、文件名和文件大小。这些信息都已</span></span><br><span class="line"><span class="comment">经被插件组装在 files 数组里返回，代码如下：</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">var</span> Upload = <span class="function"><span class="keyword">function</span>(<span class="params"> uploadType, fileName, fileSize </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.uploadType = uploadType; </span><br><span class="line">    <span class="keyword">this</span>.fileName = fileName; </span><br><span class="line">    <span class="keyword">this</span>.fileSize = fileSize; </span><br><span class="line">    <span class="keyword">this</span>.dom= <span class="literal">null</span>; </span><br><span class="line">&#125;; </span><br><span class="line">Upload.prototype.init = <span class="function"><span class="keyword">function</span>(<span class="params"> id </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> that = <span class="keyword">this</span>; </span><br><span class="line">    <span class="keyword">this</span>.id = id; </span><br><span class="line">    <span class="keyword">this</span>.dom = <span class="built_in">document</span>.createElement( <span class="string">'div'</span> ); </span><br><span class="line">    <span class="keyword">this</span>.dom.innerHTML = </span><br><span class="line"> <span class="string">'&lt;span&gt;文件名称:'</span>+ <span class="keyword">this</span>.fileName +<span class="string">', 文件大小: '</span>+ <span class="keyword">this</span>.fileSize +<span class="string">'&lt;/span&gt;'</span> + </span><br><span class="line"> <span class="string">'&lt;button class="delFile"&gt;删除&lt;/button&gt;'</span>; </span><br><span class="line">    <span class="keyword">this</span>.dom.querySelector( <span class="string">'.delFile'</span> ).onclick = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        that.delFile(); </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="built_in">document</span>.body.appendChild( <span class="keyword">this</span>.dom ); </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">同样为了简化示例，我们暂且去掉了 upload 对象的其他功能，只保留删除文件的功能，对应</span></span><br><span class="line"><span class="comment">的方法是 Upload.prototype.delFile。该方法中有一个逻辑：当被删除的文件小于 3000 KB 时，该文件将被直接删除。否则页面中会弹出一个提示框，提示用户是否确认要删除该文件。</span></span><br><span class="line"><span class="comment">接下来分别创建 3 个插件上传对象和 3 个 Flash 上传对象：</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">Upload.prototype.delFile = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( <span class="keyword">this</span>.fileSize &lt; <span class="number">3000</span> )&#123; </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.dom.parentNode.removeChild( <span class="keyword">this</span>.dom ); </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">if</span> ( <span class="built_in">window</span>.confirm( <span class="string">'确定要删除该文件吗? '</span> + <span class="keyword">this</span>.fileName ) )&#123; </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.dom.parentNode.removeChild( <span class="keyword">this</span>.dom );</span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line">startUpload( <span class="string">'plugin'</span>, [ </span><br><span class="line">    &#123; </span><br><span class="line">        fileName: <span class="string">'1.txt'</span>, </span><br><span class="line">        fileSize: <span class="number">1000</span> </span><br><span class="line">    &#125;, </span><br><span class="line">    &#123; </span><br><span class="line">        fileName: <span class="string">'2.html'</span>, </span><br><span class="line">        fileSize: <span class="number">3000</span> </span><br><span class="line">    &#125;, </span><br><span class="line">    &#123; </span><br><span class="line">        fileName: <span class="string">'3.txt'</span>, </span><br><span class="line">        fileSize: <span class="number">5000</span> </span><br><span class="line">    &#125; </span><br><span class="line">]); </span><br><span class="line">startUpload( <span class="string">'flash'</span>, [ </span><br><span class="line">    &#123; </span><br><span class="line">        fileName: <span class="string">'4.txt'</span>, </span><br><span class="line">        fileSize: <span class="number">1000</span> </span><br><span class="line">    &#125;, </span><br><span class="line">    &#123; </span><br><span class="line">        fileName: <span class="string">'5.html'</span>, </span><br><span class="line">        fileSize: <span class="number">3000</span> </span><br><span class="line">    &#125;, </span><br><span class="line">    &#123; </span><br><span class="line">        fileName: <span class="string">'6.txt'</span>, </span><br><span class="line">        fileSize: <span class="number">5000</span> </span><br><span class="line">    &#125; </span><br><span class="line">]);</span><br></pre></td></tr></table></figure>

<h3 id="享元重构"><a href="#享元重构" class="headerlink" title="享元重构"></a>享元重构</h3><p>来我们用享元模式重构它。 首先，我们需要确认插件类型 uploadType 是内部状态，那为什么单单 uploadType 是内部状态 呢？前面讲过，划分内部状态和外部状态的关键主要有以下几点。 </p>
<p>1、 内部状态储存于对象内部。 2、内部状态可以被一些对象共享。 3、内部状态独立于具体的场景，通常不会改变。4、外部状态取决于具体的场景，并根据场景而变化，外部状态不能被共享。</p>
<p>在文件上传的例子里，upload 对象必须依赖 uploadType 属性才能工作，这是因为插件上传、 Flash 上传、表单上传的实际工作原理有很大的区别，它们各自调用的接口也是完全不一样的， 必须在对象创建之初就明确它是什么类型的插件，才可以在程序的运行过程中，让它们分别调用 各自的 start、pause、cancel、del 等方法。 实际上在微云的真实代码中，虽然插件和 Flash 上传对象最终创建自一个大的工厂类，但它们实际上根据 uploadType 值的不同，分别是来自于两个不同类的对象。（在目前的例子中，为了 简化代码，我们把插件和 Flash 的构造函数合并成了一个。） 一旦明确了 uploadType，无论我们使用什么方式上传，这个上传对象都是可以被任何文件共 用的。而 fileName 和 fileSize 是根据场景而变化的，每个文件的 fileName 和 fileSize 都不一样， fileName 和 fileSize 没有办法被共享，它们只能被划分为外部状态。</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*明确了 uploadType 作为内部状态之后，我们再把其他的外部状态从构造函数中抽离出来，</span></span><br><span class="line"><span class="comment">Upload 构造函数中只保留 uploadType 参数：*/</span></span><br><span class="line"><span class="keyword">var</span> Upload = <span class="function"><span class="keyword">function</span>(<span class="params"> uploadType</span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.uploadType = uploadType; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">/*Upload.prototype.init函数也不再需要，因为upload对象初始化的工作被放在了uploadManager.add 函数里面，接下来只需要定义 Upload.prototype.del 函数即可*/</span></span><br><span class="line">Upload.prototype.delFile = <span class="function"><span class="keyword">function</span>(<span class="params"> id </span>)</span>&#123; </span><br><span class="line">    uploadManager.setExternalState( id, <span class="keyword">this</span> ); <span class="comment">// (1) </span></span><br><span class="line">    <span class="keyword">if</span> ( <span class="keyword">this</span>.fileSize &lt; <span class="number">3000</span> )&#123; </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.dom.parentNode.removeChild( <span class="keyword">this</span>.dom ); </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">if</span> ( <span class="built_in">window</span>.confirm( <span class="string">'确定要删除该文件吗? '</span> + <span class="keyword">this</span>.fileName ) )&#123; </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.dom.parentNode.removeChild( <span class="keyword">this</span>.dom ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">在开始删除文件之前，需要读取文件的实际大小，而文件的实际大小被储存在外部管理器</span></span><br><span class="line"><span class="comment">uploadManager 中，所以在这里需要通过 uploadManager.setExternalState 方法给共享对象设置正确</span></span><br><span class="line"><span class="comment">的 fileSize，上段代码中的(1)处表示把当前 id 对应的对象的外部状态都组装到共享对象中。</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="comment">//接下来定义一个工厂来创建 upload 对象，如果某种内部状态对应的共享对象已经被创建过，那么直接返回这个对象，否则创建一个新的对象；这里的UploadFactory定义为自执行函数的返回值，匿名自执行函数是为了模拟块级作用域。</span></span><br><span class="line"><span class="keyword">var</span> UploadFactory = (<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> createdFlyWeightObjs = &#123;&#125;; </span><br><span class="line">    <span class="keyword">return</span> &#123; </span><br><span class="line">        create: <span class="function"><span class="keyword">function</span>(<span class="params">uploadType</span>)</span>&#123; </span><br><span class="line">            <span class="keyword">if</span> ( createdFlyWeightObjs [uploadType] )&#123; </span><br><span class="line">                <span class="keyword">return</span> createdFlyWeightObjs [uploadType]; </span><br><span class="line">            &#125; </span><br><span class="line">            <span class="keyword">return</span> createdFlyWeightObjs [uploadType] = <span class="keyword">new</span> Upload(uploadType); </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;)();</span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">现在我们来完善前面提到的 uploadManager 对象，它负责向 UploadFactory 提交创建对象的请</span></span><br><span class="line"><span class="comment">求，并用一个 uploadDatabase 对象保存所有 upload 对象的外部状态，以便在程序运行过程中给</span></span><br><span class="line"><span class="comment">upload 共享对象设置外部状态，</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">var</span> uploadManager = (<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> uploadDatabase = &#123;&#125;; </span><br><span class="line">    <span class="keyword">return</span> &#123; </span><br><span class="line">        add: <span class="function"><span class="keyword">function</span>(<span class="params"> id, uploadType, fileName, fileSize </span>)</span>&#123; </span><br><span class="line">            <span class="keyword">var</span> flyWeightObj = UploadFactory.create( uploadType ); </span><br><span class="line">            <span class="keyword">var</span> dom = <span class="built_in">document</span>.createElement( <span class="string">'div'</span> ); </span><br><span class="line">            dom.innerHTML = </span><br><span class="line"> <span class="string">'&lt;span&gt;文件名称:'</span>+ fileName +<span class="string">', 文件大小: '</span>+ fileSize +<span class="string">'&lt;/span&gt;'</span> + </span><br><span class="line"> <span class="string">'&lt;button class="delFile"&gt;删除&lt;/button&gt;'</span>; </span><br><span class="line">            dom.querySelector( <span class="string">'.delFile'</span> ).onclick = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">                flyWeightObj.delFile( id ); </span><br><span class="line">            &#125;</span><br><span class="line">            <span class="built_in">document</span>.body.appendChild( dom ); </span><br><span class="line">            uploadDatabase[ id ] = &#123; </span><br><span class="line">                fileName: fileName, </span><br><span class="line">                fileSize: fileSize, </span><br><span class="line">                dom: dom </span><br><span class="line">            &#125;; </span><br><span class="line">            <span class="keyword">return</span> flyWeightObj ; </span><br><span class="line">        &#125;, </span><br><span class="line">        setExternalState: <span class="function"><span class="keyword">function</span>(<span class="params"> id, flyWeightObj </span>)</span>&#123; </span><br><span class="line">            <span class="keyword">var</span> uploadData = uploadDatabase[ id ]; </span><br><span class="line">            <span class="keyword">for</span> ( <span class="keyword">var</span> i <span class="keyword">in</span> uploadData )&#123; </span><br><span class="line">                flyWeightObj[ i ] = uploadData[ i ]; </span><br><span class="line">            &#125; </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;)(); </span><br><span class="line"><span class="comment">//然后是开始触发上传动作的 startUpload 函数：</span></span><br><span class="line"><span class="keyword">var</span> id = <span class="number">0</span>; </span><br><span class="line"><span class="built_in">window</span>.startUpload = <span class="function"><span class="keyword">function</span>(<span class="params"> uploadType, files </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">for</span> ( <span class="keyword">var</span> i = <span class="number">0</span>, file; file = files[ i++ ]; )&#123; </span><br><span class="line">        <span class="keyword">var</span> uploadObj = uploadManager.add( ++id, uploadType, file.fileName, file.fileSize ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h2 id="适用性"><a href="#适用性" class="headerlink" title="适用性"></a>适用性</h2><p>享元模式是一种很好的性能优化方案，但它也会带来一些复杂性的问题，从前面两组代码的 比较可以看到，使用了享元模式之后，我们需要分别多维护一个 factory 对象和一个 manager 对 象，在大部分不必要使用享元模式的环境下，这些开销是可以避免的。 享元模式带来的好处很大程度上取决于如何使用以及何时使用，一般来说，以下情况发生时 便可以使用享元模式</p>
<p>1、 一个程序中使用了大量的相似对象。 2、 由于使用了大量对象，造成很大的内存开销。 3、 对象的大多数状态都可以变为外部状态。 4、 剥离出对象的外部状态之后，可以用相对较少的共享对象取代大量对象</p>
<p>可以看到，文件上传的例子完全符合这四点。</p>
<p>我们知道，实现享元模式的关键是把内部状态和外部状态分离开来。有多少种内部状 态的组合，系统中便最多存在多少个共享对象，而外部状态储存在共享对象的外部，在必要时被 传入共享对象来组装成一个完整的对象。现在来考虑两种极端的情况，即对象没有外部状态和没有内部状态的时候。</p>
<p>没有内部状态时：构造函数 Upload 就变成了无参数的形式；其他属性如 fileName、fileSize、dom 依然可以作为外部状态保存在共享对象外部。在 uploadType 作为内部状态的时候，它可能为控件，也可能为 Flash，所以当时最多可以组合出两个 共享对象。而现在已经没有了内部状态，这意味着只需要唯一的一个共享对象。</p>
<p>管理器部分的代码不需要改动，还是负责剥离和组装外部状态。可以看到，当对象没有内部 状态的时候，生产共享对象的工厂实际上变成了一个单例工厂。虽然这时候的共享对象没有内部 状态的区分，但还是有剥离外部状态的过程，我们依然倾向于称之为享元模式</p>
<p>没有外部状态时：没有外部状态的剥离，只有共享则不能称为享元模式。</p>
<h2 id="对象池"><a href="#对象池" class="headerlink" title="对象池"></a>对象池</h2><p>对象池维护一个装载空闲对象的池子，如果需要对象的时候，不是直接 new，而是转从对象池里获取。如 果对象池里没有空闲对象，则创建一个新的对象，当获取出的对象完成它的职责之后， 再进入 池子等待被下次获取。 对象池的原理很好理解，比如我们组人手一本《JavaScript 权威指南》，从节约的角度来讲， 这并不是很划算，因为大部分时间这些书都被闲置在各自的书架上，所以我们一开始就只买一本， 或者一起建立一个小型图书馆（对象池），需要看书的时候就从图书馆里借，看完了之后再把书 还回图书馆。</p>
<p>如果同时有三个人要看这本书，而现在图书馆里只有两本，那我们再马上去书店买 一本放入图书馆。对象池技术的应用非常广泛，<strong>HTTP 连接池和数据库连接池</strong>都是其代表应用。在 Web 前端开 发中，对象池使用最多的场景大概就是跟 DOM 有关的操作。很多空间和时间都消耗在了 DOM 节点上，如何避免频繁地创建和删除 DOM 节点就成了一个有意义的话题。</p>
<p>我们可以在对象池工厂里，把创建对象的具体过程封装起来，实现一个通用的对象池：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> objectPoolFactory = <span class="function"><span class="keyword">function</span>(<span class="params"> createObjFn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> objectPool = []; </span><br><span class="line">    <span class="keyword">return</span> &#123; </span><br><span class="line">        create: <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">            <span class="keyword">var</span> obj = objectPool.length === <span class="number">0</span> ? </span><br><span class="line">                createObjFn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ) : objectPool.shift(); </span><br><span class="line">            <span class="keyword">return</span> obj; </span><br><span class="line">        &#125;, </span><br><span class="line">        recover: <span class="function"><span class="keyword">function</span>(<span class="params"> obj </span>)</span>&#123; </span><br><span class="line">            objectPool.push( obj );</span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">//现在利用 objectPoolFactory 来创建一个装载一些 iframe 的对象池：</span></span><br><span class="line"><span class="keyword">var</span> iframeFactory = objectPoolFactory( <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> iframe = <span class="built_in">document</span>.createElement( <span class="string">'iframe'</span> ); </span><br><span class="line">    <span class="built_in">document</span>.body.appendChild( iframe ); </span><br><span class="line">    iframe.onload = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        iframe.onload = <span class="literal">null</span>; <span class="comment">// 防止 iframe 重复加载的 bug </span></span><br><span class="line">        iframeFactory.recover( iframe ); <span class="comment">// iframe 加载完成之后回收节点</span></span><br><span class="line"> &#125; </span><br><span class="line">    <span class="keyword">return</span> iframe; </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> iframe1 = iframeFactory.create(); </span><br><span class="line">iframe1.src = <span class="string">'http:// baidu.com'</span>; </span><br><span class="line"><span class="keyword">var</span> iframe2 = iframeFactory.create(); </span><br><span class="line">iframe2.src = <span class="string">'http:// QQ.com'</span>; </span><br><span class="line">setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> iframe3 = iframeFactory.create(); </span><br><span class="line">    iframe3.src = <span class="string">'http:// 163.com'</span>; </span><br><span class="line">&#125;, <span class="number">3000</span> );</span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">对象池是另外一种性能优化方案，它跟享元模式有一些相似之处，但没有分离内部状态和外</span></span><br><span class="line"><span class="comment">部状态这个过程。本章用享元模式完成了一个文件上传的程序，其实也可以用对象池+事件委托来代替实现</span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure>

<p>享元模式是为解决性能问题而生的模式，这跟大部分模式的诞生原因都不一样。在一个存在 大量相似对象的系统中，享元模式可以很好地解决大量对象带来的性能问题。</p>
<h1 id="职责链模式"><a href="#职责链模式" class="headerlink" title="职责链模式"></a>职责链模式</h1><p>职责链模式的定义是：使多个对象都有机会处理请求，从而避免请求的发送者和接收者之间 的耦合关系，将这些对象连成一条链，并沿着这条链传递该请求，直到有一个对象处理它为止。 职责链模式的名字非常形象，一系列可能会处理请求的对象被连接成一条链，请求在这些对 象之间依次传递，直到遇到一个可以处理它的对象，我们把这些对象称为链中的节点，</p>
<p>职责链模式的最大优点：请求发送者只需要知道链中的第 一个节点，从而弱化了发送者和一组接收者之间的强联系。</p>
<h2 id="简单实例"><a href="#简单实例" class="headerlink" title="简单实例"></a>简单实例</h2><p>假设我们负责一个售卖手机的电商网站，经过分别交纳 500 元定金和 200 元定金的两轮预定 后（订单已在此时生成），现在已经到了正式购买的阶段。 公司针对支付过定金的用户有一定的优惠政策。在正式购买后，已经支付过 500 元定金的用 户会收到 100 元的商城优惠券，200 元定金的用户可以收到 50 元的优惠券，而之前没有支付定金 的用户只能进入普通购买模式，也就是没有优惠券，且在库存有限的情况下不一定保证能买到。 我们的订单页面是 PHP 吐出的模板，在页面加载之初，PHP 会传递给页面几个字段。 </p>
<p>1、orderType：表示订单类型（定金用户或者普通购买用户），code 的值为 1 的时候是 500 元 定金用户，为 2 的时候是 200 元定金用户，为 3 的时候是普通购买用户。2、 pay：表示用户是否已经支付定金，值为 true 或者 false, 虽然用户已经下过 500 元定金的 订单，但如果他一直没有支付定金，现在只能降级进入普通购买模式。3、stock：表示当前用于普通购买的手机库存数量，已经支付过 500 元或者 200 元定金的用 户不受此限制。 下面我们把这个流程写成代码：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> order = <span class="function"><span class="keyword">function</span>(<span class="params"> orderType, pay, stock </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( orderType === <span class="number">1</span> )&#123; <span class="comment">// 500 元定金购买模式</span></span><br><span class="line">        <span class="keyword">if</span> ( pay === <span class="literal">true</span> )&#123; <span class="comment">// 已支付定金</span></span><br><span class="line">            <span class="built_in">console</span>.log( <span class="string">'500 元定金预购, 得到 100 优惠券'</span> ); </span><br><span class="line">        &#125;<span class="keyword">else</span>&#123; <span class="comment">// 未支付定金，降级到普通购买模式</span></span><br><span class="line">            <span class="keyword">if</span> ( stock &gt; <span class="number">0</span> )&#123; <span class="comment">// 用于普通购买的手机还有库存</span></span><br><span class="line">                <span class="built_in">console</span>.log( <span class="string">'普通购买, 无优惠券'</span> ); </span><br><span class="line">            &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">                <span class="built_in">console</span>.log( <span class="string">'手机库存不足'</span> ); </span><br><span class="line">            &#125; </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> ( orderType === <span class="number">2</span> )&#123; <span class="comment">// 200 元定金购买模式</span></span><br><span class="line">        <span class="keyword">if</span> ( pay === <span class="literal">true</span> )&#123; </span><br><span class="line">            <span class="built_in">console</span>.log( <span class="string">'200 元定金预购, 得到 50 优惠券'</span> ); </span><br><span class="line">        &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">            <span class="keyword">if</span> ( stock &gt; <span class="number">0</span> )&#123; </span><br><span class="line">                <span class="built_in">console</span>.log( <span class="string">'普通购买, 无优惠券'</span> ); </span><br><span class="line">            &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">                <span class="built_in">console</span>.log( <span class="string">'手机库存不足'</span> ); </span><br><span class="line">            &#125; </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> ( orderType === <span class="number">3</span> )&#123; </span><br><span class="line">        <span class="keyword">if</span> ( stock &gt; <span class="number">0</span> )&#123; </span><br><span class="line">            <span class="built_in">console</span>.log( <span class="string">'普通购买, 无优惠券'</span> ); </span><br><span class="line">        &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">            <span class="built_in">console</span>.log( <span class="string">'手机库存不足'</span> ); </span><br><span class="line">        &#125; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line">order( <span class="number">1</span> , <span class="literal">true</span>, <span class="number">500</span>); <span class="comment">// 输出： 500 元定金预购, 得到 100 优惠券</span></span><br></pre></td></tr></table></figure>

<p>上面的代码虽然实现了功能，但是乱七八糟且难以后续维护与功能的迭代、更新。</p>
<h3 id="重构"><a href="#重构" class="headerlink" title="重构"></a>重构</h3><p>现在我们采用职责链模式重构这段代码，先把 500 元订单、200 元订单以及普通购买分成 3 个函数。 接下来把 orderType、pay、stock 这 3 个字段当作参数传递给 500 元订单函数，如果该函数不 符合处理条件，则把这个请求传递给后面的 200 元订单函数，如果 200 元订单函数依然不能处理 该请求，则继续传递请求给普通购买函数，代码如下</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 500 元订单</span></span><br><span class="line"><span class="keyword">var</span> order500 = <span class="function"><span class="keyword">function</span>(<span class="params"> orderType, pay, stock </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( orderType === <span class="number">1</span> &amp;&amp; pay === <span class="literal">true</span> )&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'500 元定金预购, 得到 100 优惠券'</span> ); </span><br><span class="line">    &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">        order200( orderType, pay, stock ); <span class="comment">// 将请求传递给 200 元订单</span></span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">// 200 元订单</span></span><br><span class="line"><span class="keyword">var</span> order200 = <span class="function"><span class="keyword">function</span>(<span class="params"> orderType, pay, stock </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( orderType === <span class="number">2</span> &amp;&amp; pay === <span class="literal">true</span> )&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'200 元定金预购, 得到 50 优惠券'</span> ); </span><br><span class="line">    &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">        orderNormal( orderType, pay, stock ); <span class="comment">// 将请求传递给普通订单</span></span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">// 普通购买订单</span></span><br><span class="line"><span class="keyword">var</span> orderNormal = <span class="function"><span class="keyword">function</span>(<span class="params"> orderType, pay, stock </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( stock &gt; <span class="number">0</span> )&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'普通购买, 无优惠券'</span> ); </span><br><span class="line">    &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'手机库存不足'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">// 测试结果：</span></span><br><span class="line">order500( <span class="number">1</span> , <span class="literal">true</span>, <span class="number">500</span>); <span class="comment">// 输出：500 元定金预购, 得到 100 优惠券</span></span><br><span class="line">order500( <span class="number">1</span>, <span class="literal">false</span>, <span class="number">500</span> ); <span class="comment">// 输出：普通购买, 无优惠券</span></span><br><span class="line">order500( <span class="number">2</span>, <span class="literal">true</span>, <span class="number">500</span> ); <span class="comment">// 输出：200 元定金预购, 得到 500 优惠券</span></span><br><span class="line">order500( <span class="number">3</span>, <span class="literal">false</span>, <span class="number">500</span> ); <span class="comment">// 输出：普通购买, 无优惠券</span></span><br><span class="line">order500( <span class="number">3</span>, <span class="literal">false</span>, <span class="number">0</span> ); <span class="comment">// 输出：手机库存不足</span></span><br></pre></td></tr></table></figure>

<p>可以看到，执行结果和前面那个巨大的 order 函数完全一样，但是代码的结构已经清晰了很 多，我们把一个大函数拆分了 3 个小函数，去掉了许多嵌套的条件分支语句。 目前已经有了不小的进步，但我们不会满足于此，虽然已经把大函数拆分成了互不影响的 3 个小函数，但可以看到，请求在链条传递中的顺序非常僵硬，传递请求的代码被耦合在了业务函 数之中。</p>
<p>这依然是违反开放封闭原则的，如果有天我们要增加 300 元预订或者去掉 200 元预订，意 味着就必须改动这些业务函数内部。就像一根环环相扣打了死结的链条，如果要增加、拆除或者 移动一个节点，就必须得先砸烂这根链条。</p>
<h3 id="可拆分节点"><a href="#可拆分节点" class="headerlink" title="可拆分节点"></a>可拆分节点</h3><p>本节我们采用一种更灵活的方式，来改进上面的职责链模式，目标是让链中的各个节点可以 灵活拆分和重组。 首先需要改写一下分别表示 3 种购买模式的节点函数，我们约定，如果某个节点不能处理请 求，则返回一个特定的字符串 ‘nextSuccessor’来表示该请求需要继续往后面传递：</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> order500 = <span class="function"><span class="keyword">function</span>(<span class="params"> orderType, pay, stock </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( orderType === <span class="number">1</span> &amp;&amp; pay === <span class="literal">true</span> )&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'500 元定金预购，得到 100 优惠券'</span> ); </span><br><span class="line">    &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">        <span class="keyword">return</span> <span class="string">'nextSuccessor'</span>; <span class="comment">// 我不知道下一个节点是谁，反正把请求往后面传递</span></span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> order200 = <span class="function"><span class="keyword">function</span>(<span class="params"> orderType, pay, stock </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( orderType === <span class="number">2</span> &amp;&amp; pay === <span class="literal">true</span> )&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'200 元定金预购，得到 50 优惠券'</span> ); </span><br><span class="line">    &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">        <span class="keyword">return</span> <span class="string">'nextSuccessor'</span>; <span class="comment">// 我不知道下一个节点是谁，反正把请求往后面传递</span></span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> orderNormal = <span class="function"><span class="keyword">function</span>(<span class="params"> orderType, pay, stock </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">if</span> ( stock &gt; <span class="number">0</span> )&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'普通购买，无优惠券'</span> ); </span><br><span class="line">    &#125;<span class="keyword">else</span>&#123; </span><br><span class="line">        <span class="built_in">console</span>.log( <span class="string">'手机库存不足'</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">/*接下来需要把函数包装进职责链节点，我们定义一个构造函数 Chain，在 new Chain 的时候传</span></span><br><span class="line"><span class="comment">递的参数即为需要被包装的函数，同时它还拥有一个实例属性 this.successor，表示在链中的下</span></span><br><span class="line"><span class="comment">一个节点。</span></span><br><span class="line"><span class="comment">此外 Chain 的 prototype 中还有两个函数，它们的作用如下所示：*/</span></span><br><span class="line"><span class="comment">// Chain.prototype.setNextSuccessor 指定在链中的下一个节点</span></span><br><span class="line"><span class="comment">// Chain.prototype.passRequest 传递请求给某个节点</span></span><br><span class="line"><span class="keyword">var</span> Chain = <span class="function"><span class="keyword">function</span>(<span class="params"> fn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">this</span>.fn = fn; </span><br><span class="line">    <span class="keyword">this</span>.successor = <span class="literal">null</span>; </span><br><span class="line">&#125;; </span><br><span class="line">Chain.prototype.setNextSuccessor = <span class="function"><span class="keyword">function</span>(<span class="params"> successor </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>.successor = successor; </span><br><span class="line">&#125;; </span><br><span class="line">Chain.prototype.passRequest = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> ret = <span class="keyword">this</span>.fn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); </span><br><span class="line">    <span class="keyword">if</span> ( ret === <span class="string">'nextSuccessor'</span> )&#123; </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>.successor &amp;&amp; <span class="keyword">this</span>.successor.passRequest.apply( <span class="keyword">this</span>.successor, <span class="built_in">arguments</span> ); </span><br><span class="line">    &#125; </span><br><span class="line">    <span class="keyword">return</span> ret; </span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">//现在我们把 3 个订单函数分别包装成职责链的节点：</span></span><br><span class="line"><span class="keyword">var</span> chainOrder500 = <span class="keyword">new</span> Chain( order500 ); </span><br><span class="line"><span class="keyword">var</span> chainOrder200 = <span class="keyword">new</span> Chain( order200 ); </span><br><span class="line"><span class="keyword">var</span> chainOrderNormal = <span class="keyword">new</span> Chain( orderNormal ); </span><br><span class="line"><span class="comment">//然后指定节点在职责链中的顺序：</span></span><br><span class="line">chainOrder500.setNextSuccessor( chainOrder200 ); </span><br><span class="line">chainOrder200.setNextSuccessor( chainOrderNormal ); </span><br><span class="line"><span class="comment">//最后把请求传递给第一个节点：</span></span><br><span class="line">chainOrder500.passRequest( <span class="number">1</span>, <span class="literal">true</span>, <span class="number">500</span> ); <span class="comment">// 输出：500 元定金预购，得到 100 优惠券</span></span><br><span class="line">chainOrder500.passRequest( <span class="number">2</span>, <span class="literal">true</span>, <span class="number">500</span> ); <span class="comment">// 输出：200 元定金预购，得到 50 优惠券</span></span><br><span class="line">chainOrder500.passRequest( <span class="number">3</span>, <span class="literal">true</span>, <span class="number">500</span> ); <span class="comment">// 输出：普通购买，无优惠券</span></span><br><span class="line">chainOrder500.passRequest( <span class="number">1</span>, <span class="literal">false</span>, <span class="number">0</span> ); <span class="comment">// 输出：手机库存不足</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*通过改进，我们可以自由灵活地增加、移除和修改链中的节点顺序，假如某天网站运营人员</span></span><br><span class="line"><span class="comment">又想出了支持 300 元定金购买，那我们就在该链中增加一个节点即可：*/</span></span><br><span class="line"><span class="keyword">var</span> order300 = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line"> <span class="comment">// 具体实现略 </span></span><br><span class="line">&#125;; </span><br><span class="line">chainOrder300= <span class="keyword">new</span> Chain( order300 ); </span><br><span class="line">chainOrder500.setNextSuccessor( chainOrder300); </span><br><span class="line">chainOrder300.setNextSuccessor( chainOrder200); </span><br><span class="line"><span class="comment">/*对于程序员来说，我们总是喜欢去改动那些相对容易改动的地方，就像改动框架的配置文件</span></span><br><span class="line"><span class="comment">远比改动框架的源代码简单得多。在这里完全不用理会原来的订单函数代码，我们要做的只是增</span></span><br><span class="line"><span class="comment">加一个节点，然后重新设置链中相关节点的顺序。 */</span></span><br></pre></td></tr></table></figure>

<h2 id="异步职责链"><a href="#异步职责链" class="headerlink" title="异步职责链"></a>异步职责链</h2><p>在上一节的职责链模式中，我们让每个节点函数同步返回一个特定的值”nextSuccessor”，来表示 是否把请求传递给下一个节点。而在现实开发中，我们经常会遇到一些异步的问题，比如我们要在 节点函数中发起一个 ajax异步请求，异步请求返回的结果才能决定是否继续在职责链中 passRequest。 这时候让节点函数同步返回”nextSuccessor”已经没有意义了，所以要给 Chain 类再增加一个原型方法 Chain.prototype.next，表示手动传递请求给职责链中的下一个节点</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">Chain.prototype.next= <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>.successor &amp;&amp; <span class="keyword">this</span>.successor.passRequest.apply( <span class="keyword">this</span>.successor, <span class="built_in">arguments</span> ); </span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">//下面是一个异步职责链的例子：</span></span><br><span class="line"><span class="keyword">var</span> fn1 = <span class="keyword">new</span> Chain(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="number">1</span> ); </span><br><span class="line">    <span class="keyword">return</span> <span class="string">'nextSuccessor'</span>; </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> fn2 = <span class="keyword">new</span> Chain(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="number">2</span> ); </span><br><span class="line">    <span class="keyword">var</span> self = <span class="keyword">this</span>; </span><br><span class="line">    setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        self.next(); </span><br><span class="line">    &#125;, <span class="number">1000</span> ); </span><br><span class="line">&#125;); </span><br><span class="line"><span class="keyword">var</span> fn3 = <span class="keyword">new</span> Chain(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">    <span class="built_in">console</span>.log( <span class="number">3</span> ); </span><br><span class="line">&#125;); </span><br><span class="line">fn1.setNextSuccessor( fn2 ).setNextSuccessor( fn3 ); </span><br><span class="line">fn1.passRequest();</span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">现在我们得到了一个特殊的链条，请求在链中的节点里传递，但节点有权利决定什么时候把</span></span><br><span class="line"><span class="comment">请求交给下一个节点。可以想象，异步的职责链加上命令模式（把 ajax 请求封装成命令对象，详</span></span><br><span class="line"><span class="comment">情请参考第 9 章），我们可以很方便地创建一个异步 ajax 队列库。</span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure>

<h2 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h2><p>职责链模式的最大优点就是解耦了请求发送者和 N 个接收者之间的复杂关 系，由于不知道链中的哪个节点可以处理你发出的请求，所以你只需把请求传递给第一个节点即 可，</p>
<p>其次，使用了职责链模式之后，链中的节点对象可以灵活地拆分重组。增加或者删除一个节 点，或者改变节点在链中的位置都是轻而易举的事情。这一点我们也已经看到，在上面的例子中， 增加一种订单完全不需要改动其他订单函数中的代码。</p>
<p>职责链模式还有一个优点，那就是可以手动指定起始节点，请求并不是非得从链中的第一个 节点开始传递。比如在公交车的例子中，如果我明确在我前面的第一个人不是售票员，那我当然 可以越过他把公交卡递给他前面的人，这样可以减少请求在链中的传递次数，更快地找到合适的 请求接受者。这在普通的条件分支语句下是做不到的，我们没有办法让请求越过某一个 if 判断。</p>
<h2 id="AOP实现"><a href="#AOP实现" class="headerlink" title="AOP实现"></a>AOP实现</h2><p>AOP是面向切面编程：面向切面的程序设计（Aspect-oriented programming，AOP，又译作面向方面的程序设计、剖面导向程序设计）是计算机科学中的一种程序设计思想，旨在将横切关注点与业务主体进行进一步分离，以提高程序代码的模块化程度。面向切面的程序设计思想也是面向切面软件开发的基础。切面的概念源于对面向对象的程序设计和计算反射的融合，但并不只限于此，它还可以用来改进传统的函数。与切面相关的编程概念还包括元对象协议、主题（Subject）、混入（Mixin）和委托（Delegate）。</p>
<p>面向切面编程为我们提供了一种将代码注入现有函数或对象的方法，而无需修改目标逻辑。</p>
<p>注入的代码虽然不是必需的，但在具有横切关注点的，例如添加日志记录功能、调试元数据、性能统计、安全控制、事务处理、异常处理或不那么通用的功能，可以在不影响原始代码的情况下注入额外的行为。把它们抽离出来，用“动态”插入的方式嵌到各业务逻辑中。业务模块可以变得比较干净，不受污染，同时这些功能点能够得到很好的复用，给模块解耦。</p>
<p>在之前的职责链实现中，我们利用了一个 Chain 类来把普通函数包装成职责链的节点。其实利用 JavaScript 的函数式特性，有一种更加方便的方法来创建职责链。 </p>
<p>下面我们改写一下 3.2.3 节 Function.prototype.after 函数，使得第一个函数返回’nextSuccessor’ 时，将请求继续传递给下一个函数，无论是返回字符串’nextSuccessor’或者 false 都只是一个约 定，当然在这里我们也可以让函数返回 false 表示传递请求，选择’nextSuccessor’字符串是因为 它看起来更能表达我们的目的，代码如下</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Function</span>.prototype.after = <span class="function"><span class="keyword">function</span>(<span class="params"> fn </span>)</span>&#123; </span><br><span class="line">    <span class="keyword">var</span> self = <span class="keyword">this</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123; </span><br><span class="line">        <span class="keyword">var</span> ret = self.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); </span><br><span class="line">        <span class="keyword">if</span> ( ret === <span class="string">'nextSuccessor'</span> )&#123; </span><br><span class="line">            <span class="keyword">return</span> fn.apply( <span class="keyword">this</span>, <span class="built_in">arguments</span> ); </span><br><span class="line">        &#125; </span><br><span class="line">        <span class="keyword">return</span> ret; </span><br><span class="line">    &#125; </span><br><span class="line">&#125;; </span><br><span class="line"><span class="keyword">var</span> order = order500yuan.after( order200yuan ).after( orderNormal ); </span><br><span class="line">order( <span class="number">1</span>, <span class="literal">true</span>, <span class="number">500</span> ); <span class="comment">// 输出：500 元定金预购，得到 100 优惠券</span></span><br><span class="line">order( <span class="number">2</span>, <span class="literal">true</span>, <span class="number">500</span> ); <span class="comment">// 输出：200 元定金预购，得到 50 优惠券</span></span><br><span class="line">order( <span class="number">1</span>, <span class="literal">false</span>, <span class="number">500</span> ); <span class="comment">// 输出：普通购买，无优惠券</span></span><br><span class="line"><span class="comment">/*用 AOP 来实现职责链既简单又巧妙，但这种把函数叠在一起的方式，同时也叠加了函数的</span></span><br><span class="line"><span class="comment">作用域，如果链条太长的话，也会对性能有较大的影响。*/</span></span><br></pre></td></tr></table></figure>

<p>在 JavaScript 开发中，职责链模式是最容易被忽视的模式之一。实际上只要运用得当，职责 链模式可以很好地帮助我们管理代码，降低发起请求的对象和处理请求的对象之间的耦合性。职 责链中的节点数量和顺序是可以自由变化的，我们可以在运行时决定链中包含哪些节点。 无论是作用域链、原型链，还是 DOM 节点中的事件冒泡，我们都能从中找到职责链模式的 影子。职责链模式还可以和组合模式结合在一起，用来连接部件和父部件，或是提高组合对象的 效率。</p>

      
    </div>
    
    
    
    
    <div>
      
    </div>
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  


  
  <nav class="pagination">
    <a class="extend prev" rel="prev" href="/"><i class="fa fa-angle-left" aria-label="上一页"></i></a><a class="page-number" href="/">1</a><span class="page-number current">2</span><a class="page-number" href="/page/3/">3</a><span class="space">&hellip;</span><a class="page-number" href="/page/10/">10</a><a class="extend next" rel="next" href="/page/3/"><i class="fa fa-angle-right" aria-label="下一页"></i></a>
  </nav>



          </div>
          

<script>
  window.addEventListener('tabs:register', () => {
    let { activeClass } = CONFIG.comments;
    if (CONFIG.comments.storage) {
      activeClass = localStorage.getItem('comments_active') || activeClass;
    }
    if (activeClass) {
      let activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`);
      if (activeTab) {
        activeTab.click();
      }
    }
  });
  if (CONFIG.comments.storage) {
    window.addEventListener('tabs:click', event => {
      if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return;
      let commentClass = event.target.classList[1];
      localStorage.setItem('comments_active', commentClass);
    });
  }
</script>

        </div>
          
  
  <div class="toggle sidebar-toggle">
    <span class="toggle-line toggle-line-first"></span>
    <span class="toggle-line toggle-line-middle"></span>
    <span class="toggle-line toggle-line-last"></span>
  </div>

  <aside class="sidebar">
    <div class="sidebar-inner">

      <ul class="sidebar-nav motion-element">
        <li class="sidebar-nav-toc">
          文章目录
        </li>
        <li class="sidebar-nav-overview">
          站点概览
        </li>
      </ul>

      <!--noindex-->
      <div class="post-toc-wrap sidebar-panel">
      </div>
      <!--/noindex-->

      <div class="site-overview-wrap sidebar-panel">
        <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
    <img class="site-author-image" itemprop="image" alt="Li Yudong"
      src="/images/author.jpg">
  <p class="site-author-name" itemprop="name">Li Yudong</p>
  <div class="site-description" itemprop="description"></div>
</div>
<div class="site-state-wrap motion-element">
  <nav class="site-state">
      <div class="site-state-item site-state-posts">
          <a href="/archives/">
        
          <span class="site-state-item-count">95</span>
          <span class="site-state-item-name">日志</span>
        </a>
      </div>
      <div class="site-state-item site-state-categories">
            <a href="/categories/">
          
        <span class="site-state-item-count">14</span>
        <span class="site-state-item-name">分类</span></a>
      </div>
      <div class="site-state-item site-state-tags">
            <a href="/tags/">
          
        <span class="site-state-item-count">18</span>
        <span class="site-state-item-name">标签</span></a>
      </div>
  </nav>
</div>
  <div class="links-of-author motion-element">
      <span class="links-of-author-item">
        <a href="https://github.com/tiarmor1" title="GitHub → https:&#x2F;&#x2F;github.com&#x2F;tiarmor1" rel="noopener" target="_blank"><i class="fab fa-github fa-fw"></i>GitHub</a>
      </span>
      <span class="links-of-author-item">
        <a href="mailto:1157019137@qq.com" title="E-Mail → mailto:1157019137@qq.com" rel="noopener" target="_blank"><i class="fa fa-envelope fa-fw"></i>E-Mail</a>
      </span>
  </div>
  <div class="cc-license motion-element" itemprop="license">
    <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" class="cc-opacity" rel="noopener" target="_blank"><img src="/images/cc-by-nc-sa.svg" alt="Creative Commons"></a>
  </div>



      </div>

    </div>
  </aside>
  <div id="sidebar-dimmer"></div>


      </div>
    </main>

    <footer class="footer">
      <div class="footer-inner">
        

        

<div class="copyright">
  
  &copy; 2020 – 
  <span itemprop="copyrightYear">2022</span>
  <span class="with-love">
    <i class="fa fa-heart"></i>
  </span>
  <span class="author" itemprop="copyrightHolder">Li Yudong</span>
</div>

        








      </div>
    </footer>
  </div>

  
  
  <script color='0,0,0' opacity='0.5' zIndex='-1' count='150' src="/lib1/canvas-nest/canvas-nest.min.js"></script>
  <script src="/lib1/anime.min.js"></script>
  <script src="/lib1/velocity/velocity.min.js"></script>
  <script src="/lib1/velocity/velocity.ui.min.js"></script>

<script src="/js/utils.js"></script>

<script src="/js/motion.js"></script>


<script src="/js/schemes/pisces.js"></script>


<script src="/js/next-boot.js"></script>




  




  
<script src="/js/local-search.js"></script>













  

  

  
   <canvas class="fireworks" style="position: fixed;left: 0;top: 0;z-index: 1; pointer-events: none;" ></canvas> 
   <script type="text/javascript" src="//cdn.bootcss.com/animejs/2.2.0/anime.min.js"></script> 
   <script type="text/javascript" src="/js/fireworks.js"></script>
  
</body>
</html>

